From ede6233355b70ac831aa79949e0b396ba6320b99 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 4 Aug 2021 11:55:02 -0700 Subject: [PATCH 001/100] The prototype --- log2table.awk | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 log2table.awk diff --git a/log2table.awk b/log2table.awk new file mode 100644 index 0000000..5188c81 --- /dev/null +++ b/log2table.awk @@ -0,0 +1,75 @@ + +# TODO: Implement quoting properly +function quote_html(s) { return s; } +function quote_html_attribute(s) { return gensub("\"", """, "g", s) } + +BEGIN { + FS = ":" + num_rows = 0 + num_cols = 0 + read_header = 1 + # h/t https://wabain.github.io/2019/10/13/css-rotated-table-header.html + print "" +} + +END { + printf "" + for (i=0; i < num_cols; i++) { + printf "", quote_html_attribute(col_headers[i]), quote_html(col_headers[i]) + } + print ""; + for (i=1*skip_rows; i <= num_rows; i++) { + spacer_row = substr(row_headers[i], 1, 1) == "!" + row_header = spacer_row ? substr(row_headers[i], 2) : row_headers[i] + printf "", quote_html_attribute(row_header), quote_html(row_header) + for (j = 0;j < num_cols; j++) { + printf "" + } + print "" + } + print "
%s
%s" rows[i][j] "
" +} + +{ + if (/^$/) { + read_header = 1 + num_rows++ + } else { + if (read_header) { + read_header = 0 + row_headers[num_rows] = $0 + } else { + if (! ($1 in col_index)) { + col_headers[num_cols] = $1 + col_index[$1] = num_cols++ + } + rows[num_rows][col_index[$1]] = $2 ? $2 : " " + } + } +} -- 2.50.1 From 7e91db8fa22586917fb41a9cf94cdb5dabf2a70a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sun, 18 Aug 2024 22:56:43 -0700 Subject: [PATCH 002/100] Switch to rust --- .gitignore | 1 + Cargo.lock | 7 +++++ Cargo.toml | 10 +++++++ log2table.awk | 75 --------------------------------------------------- src/main.rs | 3 +++ 5 files changed, 21 insertions(+), 75 deletions(-) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 log2table.awk create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4ea95ca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "tablify" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..28acdc3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tablify" +version = "0.1.0" +edition = "2021" +authors = ["Scott Worley "] +description = "Summarize a text log as an HTML table" +license = "GPL-3.0" +repository = "https://git.scottworley.com/tablify" + +[dependencies] diff --git a/log2table.awk b/log2table.awk deleted file mode 100644 index 5188c81..0000000 --- a/log2table.awk +++ /dev/null @@ -1,75 +0,0 @@ - -# TODO: Implement quoting properly -function quote_html(s) { return s; } -function quote_html_attribute(s) { return gensub("\"", """, "g", s) } - -BEGIN { - FS = ":" - num_rows = 0 - num_cols = 0 - read_header = 1 - # h/t https://wabain.github.io/2019/10/13/css-rotated-table-header.html - print "" -} - -END { - printf "" - for (i=0; i < num_cols; i++) { - printf "", quote_html_attribute(col_headers[i]), quote_html(col_headers[i]) - } - print ""; - for (i=1*skip_rows; i <= num_rows; i++) { - spacer_row = substr(row_headers[i], 1, 1) == "!" - row_header = spacer_row ? substr(row_headers[i], 2) : row_headers[i] - printf "", quote_html_attribute(row_header), quote_html(row_header) - for (j = 0;j < num_cols; j++) { - printf "" - } - print "" - } - print "
%s
%s" rows[i][j] "
" -} - -{ - if (/^$/) { - read_header = 1 - num_rows++ - } else { - if (read_header) { - read_header = 0 - row_headers[num_rows] = $0 - } else { - if (! ($1 in col_index)) { - col_headers[num_cols] = $1 - col_index[$1] = num_cols++ - } - rows[num_rows][col_index[$1]] = $2 ? $2 : " " - } - } -} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} -- 2.50.1 From ece97615567c7bc537f960d1784d652b11c0e66c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sun, 18 Aug 2024 23:13:03 -0700 Subject: [PATCH 003/100] Separate library & executable --- src/lib.rs | 3 +++ src/main.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..820f43e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub fn tablify(_input: &impl std::io::Read) -> String { + String::from("Hello, world!") +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..95f468f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,3 @@ fn main() { - println!("Hello, world!"); + print!("{}", tablify::tablify(&std::io::stdin())); } -- 2.50.1 From 75bb888a36b5fdc005a0661c964779d6e0277298 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sun, 18 Aug 2024 23:46:55 -0700 Subject: [PATCH 004/100] read_rows() lies --- src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 820f43e..0e2007d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,37 @@ +#[cfg(test)] +use std::iter::Iterator; + +#[derive(Debug, PartialEq, Eq)] +struct RowInput { + label: String, + entries: Vec, +} + +#[cfg(test)] +fn read_rows(_input: &impl std::io::Read) -> impl Iterator { + vec![RowInput { + label: String::from("foo"), + entries: vec![], + }] + .into_iter() +} + pub fn tablify(_input: &impl std::io::Read) -> String { String::from("Hello, world!") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_rows() { + assert_eq!( + read_rows(&&b"foo"[..]).collect::>(), + vec![RowInput { + label: String::from("foo"), + entries: vec![] + }] + ); + } +} -- 2.50.1 From 9dfa98b71aca915f8daf3b5c8a4ad702a1f9e8da Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 00:12:54 -0700 Subject: [PATCH 005/100] Read one row header --- src/lib.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0e2007d..0584cd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #[cfg(test)] +use std::io::BufRead; +#[cfg(test)] use std::iter::Iterator; #[derive(Debug, PartialEq, Eq)] @@ -8,9 +10,13 @@ struct RowInput { } #[cfg(test)] -fn read_rows(_input: &impl std::io::Read) -> impl Iterator { +fn read_rows(input: impl std::io::Read) -> impl Iterator { vec![RowInput { - label: String::from("foo"), + label: std::io::BufReader::new(input) + .lines() + .nth(0) + .unwrap() + .unwrap(), entries: vec![], }] .into_iter() @@ -27,11 +33,18 @@ mod tests { #[test] fn test_read_rows() { assert_eq!( - read_rows(&&b"foo"[..]).collect::>(), + read_rows(&b"foo"[..]).collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![] }] ); + assert_eq!( + read_rows(&b"bar"[..]).collect::>(), + vec![RowInput { + label: String::from("bar"), + entries: vec![] + }] + ); } } -- 2.50.1 From 2aa9ef947fa4104deef034305f1238275427a092 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 00:17:14 -0700 Subject: [PATCH 006/100] Read multiple row headers --- src/lib.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0584cd2..1d098b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,15 +11,10 @@ struct RowInput { #[cfg(test)] fn read_rows(input: impl std::io::Read) -> impl Iterator { - vec![RowInput { - label: std::io::BufReader::new(input) - .lines() - .nth(0) - .unwrap() - .unwrap(), + std::io::BufReader::new(input).lines().map(|line| RowInput { + label: line.unwrap(), entries: vec![], - }] - .into_iter() + }) } pub fn tablify(_input: &impl std::io::Read) -> String { @@ -46,5 +41,18 @@ mod tests { entries: vec![] }] ); + assert_eq!( + read_rows(&b"foo\nbar\n"[..]).collect::>(), + vec![ + RowInput { + label: String::from("foo"), + entries: vec![] + }, + RowInput { + label: String::from("bar"), + entries: vec![] + } + ] + ); } } -- 2.50.1 From 201b9ef3e07ed5948730079475f79703b0cd28f8 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 01:38:40 -0700 Subject: [PATCH 007/100] Proper log parsing I'm bummed that this couldn't be a simple Iterator::scan(). Scan doesn't have a way to get control one last time at end-of-stream to dump the accumulator state. --- src/lib.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1d098b0..b583c3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,12 +9,49 @@ struct RowInput { entries: Vec, } +struct Reader>> { + input: Input, + row: Option, +} +impl>> Reader { + #[cfg(test)] + fn new(input: Input) -> Self { + Self { input, row: None } + } +} +impl>> Iterator for Reader { + type Item = Result; + fn next(&mut self) -> Option { + loop { + match self.input.next() { + None => return Ok(std::mem::take(&mut self.row)).transpose(), + Some(Err(e)) => return Some(Err(e)), + Some(Ok(line)) if line.is_empty() && self.row.is_some() => { + return Ok(std::mem::take(&mut self.row)).transpose() + } + Some(Ok(line)) if line.is_empty() => {} + Some(Ok(line)) if line.starts_with(' ') => match &mut self.row { + None => return Some(Err(std::io::Error::other("Entry with no header"))), + Some(ref mut row) => row.entries.push(String::from(line.trim())), + }, + Some(Ok(line)) => { + let prev = std::mem::take(&mut self.row); + self.row = Some(RowInput { + label: line, + entries: vec![], + }); + if prev.is_some() { + return Ok(prev).transpose(); + } + } + } + } + } +} + #[cfg(test)] -fn read_rows(input: impl std::io::Read) -> impl Iterator { - std::io::BufReader::new(input).lines().map(|line| RowInput { - label: line.unwrap(), - entries: vec![], - }) +fn read_rows(input: impl std::io::Read) -> impl Iterator> { + Reader::new(std::io::BufReader::new(input).lines()) } pub fn tablify(_input: &impl std::io::Read) -> String { @@ -28,21 +65,21 @@ mod tests { #[test] fn test_read_rows() { assert_eq!( - read_rows(&b"foo"[..]).collect::>(), + read_rows(&b"foo"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![] }] ); assert_eq!( - read_rows(&b"bar"[..]).collect::>(), + read_rows(&b"bar"[..]).flatten().collect::>(), vec![RowInput { label: String::from("bar"), entries: vec![] }] ); assert_eq!( - read_rows(&b"foo\nbar\n"[..]).collect::>(), + read_rows(&b"foo\nbar\n"[..]).flatten().collect::>(), vec![ RowInput { label: String::from("foo"), @@ -54,5 +91,44 @@ mod tests { } ] ); + assert_eq!( + read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), + vec![RowInput { + label: String::from("foo"), + entries: vec![String::from("bar")] + }] + ); + assert_eq!( + read_rows(&b"foo\n bar\n baz\n"[..]) + .flatten() + .collect::>(), + vec![RowInput { + label: String::from("foo"), + entries: vec![String::from("bar"), String::from("baz")] + }] + ); + assert_eq!( + read_rows(&b"foo\n\nbar\n"[..]) + .flatten() + .collect::>(), + vec![ + RowInput { + label: String::from("foo"), + entries: vec![] + }, + RowInput { + label: String::from("bar"), + entries: vec![] + } + ] + ); + + let bad = read_rows(&b" foo"[..]).next().unwrap(); + assert!(bad.is_err()); + assert!(format!("{bad:?}").contains("Entry with no header")); + + let bad2 = read_rows(&b"foo\n\n bar"[..]).nth(1).unwrap(); + assert!(bad2.is_err()); + assert!(format!("{bad2:?}").contains("Entry with no header")); } } -- 2.50.1 From 1f6bd845277daaea2513cc3dc19a0907fe9f6d6c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 01:48:16 -0700 Subject: [PATCH 008/100] Treat all-whitespace lines as blank lines As a side effect, this also removes trailing whitespace, which nice. --- src/lib.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b583c3e..72a3add 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,11 @@ impl>> Iterator for Reader type Item = Result; fn next(&mut self) -> Option { loop { - match self.input.next() { + match self + .input + .next() + .map(|r| r.map(|line| String::from(line.trim_end()))) + { None => return Ok(std::mem::take(&mut self.row)).transpose(), Some(Err(e)) => return Some(Err(e)), Some(Ok(line)) if line.is_empty() && self.row.is_some() => { @@ -122,6 +126,30 @@ mod tests { } ] ); + assert_eq!( + read_rows(&b"foo\n \nbar\n"[..]) + .flatten() + .collect::>(), + vec![ + RowInput { + label: String::from("foo"), + entries: vec![] + }, + RowInput { + label: String::from("bar"), + entries: vec![] + } + ] + ); + assert_eq!( + read_rows(&b"foo \n bar \n"[..]) + .flatten() + .collect::>(), + vec![RowInput { + label: String::from("foo"), + entries: vec![String::from("bar")] + }] + ); let bad = read_rows(&b" foo"[..]).next().unwrap(); assert!(bad.is_err()); -- 2.50.1 From 8110b492cfa41fd99bfb639d7491b4e075fa9385 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 10:49:44 -0700 Subject: [PATCH 009/100] Line numbers in error messages --- src/lib.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 72a3add..56a2b6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,13 +10,16 @@ struct RowInput { } struct Reader>> { - input: Input, + input: std::iter::Enumerate, row: Option, } impl>> Reader { #[cfg(test)] fn new(input: Input) -> Self { - Self { input, row: None } + Self { + input: input.enumerate(), + row: None, + } } } impl>> Iterator for Reader { @@ -26,19 +29,24 @@ impl>> Iterator for Reader match self .input .next() - .map(|r| r.map(|line| String::from(line.trim_end()))) + .map(|(n, r)| (n, r.map(|line| String::from(line.trim_end())))) { None => return Ok(std::mem::take(&mut self.row)).transpose(), - Some(Err(e)) => return Some(Err(e)), - Some(Ok(line)) if line.is_empty() && self.row.is_some() => { + Some((_, Err(e))) => return Some(Err(e)), + Some((_, Ok(line))) if line.is_empty() && self.row.is_some() => { return Ok(std::mem::take(&mut self.row)).transpose() } - Some(Ok(line)) if line.is_empty() => {} - Some(Ok(line)) if line.starts_with(' ') => match &mut self.row { - None => return Some(Err(std::io::Error::other("Entry with no header"))), + Some((_, Ok(line))) if line.is_empty() => {} + Some((n, Ok(line))) if line.starts_with(' ') => match &mut self.row { + None => { + return Some(Err(std::io::Error::other(format!( + "{}: Entry with no header", + n + 1 + )))) + } Some(ref mut row) => row.entries.push(String::from(line.trim())), }, - Some(Ok(line)) => { + Some((_, Ok(line))) => { let prev = std::mem::take(&mut self.row); self.row = Some(RowInput { label: line, @@ -153,10 +161,10 @@ mod tests { let bad = read_rows(&b" foo"[..]).next().unwrap(); assert!(bad.is_err()); - assert!(format!("{bad:?}").contains("Entry with no header")); + assert!(format!("{bad:?}").contains("1: Entry with no header")); let bad2 = read_rows(&b"foo\n\n bar"[..]).nth(1).unwrap(); assert!(bad2.is_err()); - assert!(format!("{bad2:?}").contains("Entry with no header")); + assert!(format!("{bad2:?}").contains("3: Entry with no header")); } } -- 2.50.1 From f272e502c4d1e2e33ab1e9fe460df69ff8f8ccd2 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 11:09:43 -0700 Subject: [PATCH 010/100] Column counts --- src/lib.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 56a2b6d..e90f66b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #[cfg(test)] +use std::collections::HashMap; +#[cfg(test)] use std::io::BufRead; #[cfg(test)] use std::iter::Iterator; @@ -66,6 +68,19 @@ fn read_rows(input: impl std::io::Read) -> impl Iterator HashMap { + rows.iter() + .flat_map(|r| r.entries.iter()) + .fold(HashMap::new(), |mut counts, e| { + counts + .entry(String::from(e)) + .and_modify(|c| *c += 1) + .or_insert(1); + counts + }) +} + pub fn tablify(_input: &impl std::io::Read) -> String { String::from("Hello, world!") } @@ -167,4 +182,24 @@ mod tests { assert!(bad2.is_err()); assert!(format!("{bad2:?}").contains("3: Entry with no header")); } + + #[test] + fn test_column_counts() { + assert_eq!( + column_counts( + &read_rows(&b"foo\n bar\n baz\n"[..]) + .collect::, _>>() + .unwrap() + ), + HashMap::from([(String::from("bar"), 1), (String::from("baz"), 1)]) + ); + assert_eq!( + column_counts( + &read_rows(&b"foo\n bar\n baz\nquux\n baz"[..]) + .collect::, _>>() + .unwrap() + ), + HashMap::from([(String::from("bar"), 1), (String::from("baz"), 2)]) + ); + } } -- 2.50.1 From 397ef957e293901c2945f815e585474608fe2c9d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 11:17:26 -0700 Subject: [PATCH 011/100] Don't count multiple entries in a single row in column counts --- src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e90f66b..c7271f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #[cfg(test)] -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; #[cfg(test)] use std::io::BufRead; #[cfg(test)] @@ -71,7 +71,7 @@ fn read_rows(input: impl std::io::Read) -> impl Iterator HashMap { rows.iter() - .flat_map(|r| r.entries.iter()) + .flat_map(|r| r.entries.iter().collect::>().into_iter()) .fold(HashMap::new(), |mut counts, e| { counts .entry(String::from(e)) @@ -201,5 +201,13 @@ mod tests { ), HashMap::from([(String::from("bar"), 1), (String::from("baz"), 2)]) ); + assert_eq!( + column_counts( + &read_rows(&b"foo\n bar\n bar\n baz\n bar\nquux\n baz"[..]) + .collect::, _>>() + .unwrap() + ), + HashMap::from([(String::from("bar"), 1), (String::from("baz"), 2)]) + ); } } -- 2.50.1 From 58b5f36de045c760efa51b21a6d841f5b62558db Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 11:28:02 -0700 Subject: [PATCH 012/100] Sort column counts --- src/lib.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c7271f3..fcf5dc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,16 +69,21 @@ fn read_rows(input: impl std::io::Read) -> impl Iterator HashMap { - rows.iter() +fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { + let mut counts: Vec<_> = rows + .iter() .flat_map(|r| r.entries.iter().collect::>().into_iter()) - .fold(HashMap::new(), |mut counts, e| { - counts - .entry(String::from(e)) - .and_modify(|c| *c += 1) + .fold(HashMap::new(), |mut cs, e| { + cs.entry(String::from(e)) + .and_modify(|n| *n += 1) .or_insert(1); - counts + cs }) + .into_iter() + .map(|(col, n)| (n, col)) + .collect(); + counts.sort(); + counts } pub fn tablify(_input: &impl std::io::Read) -> String { @@ -191,7 +196,7 @@ mod tests { .collect::, _>>() .unwrap() ), - HashMap::from([(String::from("bar"), 1), (String::from("baz"), 1)]) + vec![(1, String::from("bar")), (1, String::from("baz"))] ); assert_eq!( column_counts( @@ -199,7 +204,7 @@ mod tests { .collect::, _>>() .unwrap() ), - HashMap::from([(String::from("bar"), 1), (String::from("baz"), 2)]) + vec![(1, String::from("bar")), (2, String::from("baz"))] ); assert_eq!( column_counts( @@ -207,7 +212,7 @@ mod tests { .collect::, _>>() .unwrap() ), - HashMap::from([(String::from("bar"), 1), (String::from("baz"), 2)]) + vec![(1, String::from("bar")), (2, String::from("baz"))] ); } } -- 2.50.1 From 4b99fb70fa61addf2373a460d6d5c7fe82e19f5a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 11:45:25 -0700 Subject: [PATCH 013/100] Start connecting stuff --- src/lib.rs | 18 ++++++++++-------- src/main.rs | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fcf5dc8..8d60eb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,5 @@ -#[cfg(test)] use std::collections::{HashMap, HashSet}; -#[cfg(test)] use std::io::BufRead; -#[cfg(test)] use std::iter::Iterator; #[derive(Debug, PartialEq, Eq)] @@ -16,7 +13,6 @@ struct Reader>> { row: Option, } impl>> Reader { - #[cfg(test)] fn new(input: Input) -> Self { Self { input: input.enumerate(), @@ -63,12 +59,10 @@ impl>> Iterator for Reader } } -#[cfg(test)] fn read_rows(input: impl std::io::Read) -> impl Iterator> { Reader::new(std::io::BufReader::new(input).lines()) } -#[cfg(test)] fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { let mut counts: Vec<_> = rows .iter() @@ -86,8 +80,16 @@ fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { counts } -pub fn tablify(_input: &impl std::io::Read) -> String { - String::from("Hello, world!") +/// # Errors +/// +/// Will return `Err` if +/// * there's an i/o error while reading `input` +/// * the log has invalid syntax: +/// * an indented line with no preceding non-indented line +pub fn tablify(input: impl std::io::Read) -> Result { + let rows = read_rows(input).collect::, _>>()?; + let _columns = column_counts(&rows); + Ok(String::from("Hello, world!")) } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index 95f468f..8cf4859 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,3 @@ fn main() { - print!("{}", tablify::tablify(&std::io::stdin())); + print!("{}", tablify::tablify(std::io::stdin()).unwrap()); } -- 2.50.1 From cc2378d55111b3745a8f980f34b1f7b1126f37b1 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 12:04:29 -0700 Subject: [PATCH 014/100] HTML, CSS, & JS Copied over from the old prototype --- src/lib.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8d60eb8..c2da4c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,42 @@ use std::collections::{HashMap, HashSet}; use std::io::BufRead; use std::iter::Iterator; +const HEADER: &str = " + + + + + + + + + + "; +const FOOTER: &str = " +
+ +"; + #[derive(Debug, PartialEq, Eq)] struct RowInput { label: String, @@ -89,7 +125,7 @@ fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { pub fn tablify(input: impl std::io::Read) -> Result { let rows = read_rows(input).collect::, _>>()?; let _columns = column_counts(&rows); - Ok(String::from("Hello, world!")) + Ok(String::from(HEADER) + "Hello, world!" + FOOTER) } #[cfg(test)] -- 2.50.1 From 14e9852b53792c9f575de03d6a4349a00a4320b4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 12:21:49 -0700 Subject: [PATCH 015/100] Entry type --- src/lib.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c2da4c9..245a47a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,10 +38,13 @@ const FOOTER: &str = " "; +#[derive(Debug, PartialEq, Eq, Hash)] +struct Entry(String); + #[derive(Debug, PartialEq, Eq)] struct RowInput { label: String, - entries: Vec, + entries: Vec, } struct Reader>> { @@ -78,7 +81,7 @@ impl>> Iterator for Reader n + 1 )))) } - Some(ref mut row) => row.entries.push(String::from(line.trim())), + Some(ref mut row) => row.entries.push(Entry(String::from(line.trim()))), }, Some((_, Ok(line))) => { let prev = std::mem::take(&mut self.row); @@ -104,7 +107,7 @@ fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { .iter() .flat_map(|r| r.entries.iter().collect::>().into_iter()) .fold(HashMap::new(), |mut cs, e| { - cs.entry(String::from(e)) + cs.entry(String::from(&e.0)) .and_modify(|n| *n += 1) .or_insert(1); cs @@ -165,7 +168,7 @@ mod tests { read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), - entries: vec![String::from("bar")] + entries: vec![Entry(String::from("bar"))] }] ); assert_eq!( @@ -174,7 +177,7 @@ mod tests { .collect::>(), vec![RowInput { label: String::from("foo"), - entries: vec![String::from("bar"), String::from("baz")] + entries: vec![Entry(String::from("bar")), Entry(String::from("baz"))] }] ); assert_eq!( @@ -213,7 +216,7 @@ mod tests { .collect::>(), vec![RowInput { label: String::from("foo"), - entries: vec![String::from("bar")] + entries: vec![Entry(String::from("bar"))] }] ); -- 2.50.1 From e8657dffc83621d84240320f5389b81ab8204c78 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 12:29:52 -0700 Subject: [PATCH 016/100] Make a place in Entrty for instance data --- src/lib.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 245a47a..bebbcf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,18 @@ const FOOTER: &str = " "; #[derive(Debug, PartialEq, Eq, Hash)] -struct Entry(String); +struct Entry { + col: String, + instance: String, +} +impl From<&str> for Entry { + fn from(value: &str) -> Entry { + Entry { + col: String::from(value), + instance: String::from(""), + } + } +} #[derive(Debug, PartialEq, Eq)] struct RowInput { @@ -81,7 +92,7 @@ impl>> Iterator for Reader n + 1 )))) } - Some(ref mut row) => row.entries.push(Entry(String::from(line.trim()))), + Some(ref mut row) => row.entries.push(Entry::from(line.trim())), }, Some((_, Ok(line))) => { let prev = std::mem::take(&mut self.row); @@ -107,7 +118,7 @@ fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { .iter() .flat_map(|r| r.entries.iter().collect::>().into_iter()) .fold(HashMap::new(), |mut cs, e| { - cs.entry(String::from(&e.0)) + cs.entry(String::from(&e.col)) .and_modify(|n| *n += 1) .or_insert(1); cs @@ -168,7 +179,7 @@ mod tests { read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), - entries: vec![Entry(String::from("bar"))] + entries: vec![Entry::from("bar")] }] ); assert_eq!( @@ -177,7 +188,7 @@ mod tests { .collect::>(), vec![RowInput { label: String::from("foo"), - entries: vec![Entry(String::from("bar")), Entry(String::from("baz"))] + entries: vec![Entry::from("bar"), Entry::from("baz")] }] ); assert_eq!( @@ -216,7 +227,7 @@ mod tests { .collect::>(), vec![RowInput { label: String::from("foo"), - entries: vec![Entry(String::from("bar"))] + entries: vec![Entry::from("bar")] }] ); -- 2.50.1 From b89077705f91d010ac22f3f4cabe5336257a0758 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 12:41:49 -0700 Subject: [PATCH 017/100] Split entries at ':' to separate instance data --- src/lib.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bebbcf7..01668eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,13 +41,19 @@ const FOOTER: &str = " #[derive(Debug, PartialEq, Eq, Hash)] struct Entry { col: String, - instance: String, + instance: Option, } impl From<&str> for Entry { fn from(value: &str) -> Entry { - Entry { - col: String::from(value), - instance: String::from(""), + match value.split_once(':') { + None => Entry { + col: String::from(value), + instance: None, + }, + Some((col, instance)) => Entry { + col: String::from(col), + instance: Some(String::from(instance)), + }, } } } @@ -116,9 +122,15 @@ fn read_rows(input: impl std::io::Read) -> impl Iterator Vec<(usize, String)> { let mut counts: Vec<_> = rows .iter() - .flat_map(|r| r.entries.iter().collect::>().into_iter()) - .fold(HashMap::new(), |mut cs, e| { - cs.entry(String::from(&e.col)) + .flat_map(|r| { + r.entries + .iter() + .map(|e| &e.col) + .collect::>() + .into_iter() + }) + .fold(HashMap::new(), |mut cs, col| { + cs.entry(String::from(col)) .and_modify(|n| *n += 1) .or_insert(1); cs @@ -146,6 +158,24 @@ pub fn tablify(input: impl std::io::Read) -> Result { mod tests { use super::*; + #[test] + fn test_parse_entry() { + assert_eq!( + Entry::from("foo"), + Entry { + col: String::from("foo"), + instance: None + } + ); + assert_eq!( + Entry::from("foo:bar"), + Entry { + col: String::from("foo"), + instance: Some(String::from("bar")) + } + ); + } + #[test] fn test_read_rows() { assert_eq!( @@ -266,5 +296,13 @@ mod tests { ), vec![(1, String::from("bar")), (2, String::from("baz"))] ); + assert_eq!( + column_counts( + &read_rows(&b"foo\n bar: 1\n bar: 2\n baz\n bar\nquux\n baz"[..]) + .collect::, _>>() + .unwrap() + ), + vec![(1, String::from("bar")), (2, String::from("baz"))] + ); } } -- 2.50.1 From 0d999bc3cb6e12f158f71a2a28fecfa992e09b47 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 12:43:34 -0700 Subject: [PATCH 018/100] Trim whitespace when parsing entries --- src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 01668eb..5b06e70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,8 +51,8 @@ impl From<&str> for Entry { instance: None, }, Some((col, instance)) => Entry { - col: String::from(col), - instance: Some(String::from(instance)), + col: String::from(col.trim()), + instance: Some(String::from(instance.trim())), }, } } @@ -174,6 +174,13 @@ mod tests { instance: Some(String::from("bar")) } ); + assert_eq!( + Entry::from("foo: bar"), + Entry { + col: String::from("foo"), + instance: Some(String::from("bar")) + } + ); } #[test] -- 2.50.1 From d22b2e05706f7a4367ac3df33d61383673724b8b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 12:55:11 -0700 Subject: [PATCH 019/100] column_order() --- src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5b06e70..2dd3964 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,6 +141,12 @@ fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { counts.sort(); counts } +fn column_order(rows: &[RowInput]) -> Vec { + column_counts(rows) + .into_iter() + .map(|(_, col)| col) + .collect() +} /// # Errors /// @@ -150,7 +156,7 @@ fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { /// * an indented line with no preceding non-indented line pub fn tablify(input: impl std::io::Read) -> Result { let rows = read_rows(input).collect::, _>>()?; - let _columns = column_counts(&rows); + let _columns = column_order(&rows); Ok(String::from(HEADER) + "Hello, world!" + FOOTER) } -- 2.50.1 From de408c29be1f465f08fc0ba4114704d3ef8bdcef Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 13:36:15 -0700 Subject: [PATCH 020/100] Render table --- src/lib.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2dd3964..9504c7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,6 +148,44 @@ fn column_order(rows: &[RowInput]) -> Vec { .collect() } +fn render_instance(entry: &Entry) -> String { + match &entry.instance { + None => String::from("✓ "), + Some(instance) => String::from(instance) + " ", + } +} + +fn render_cell(col: &str, row: &RowInput) -> String { + // TODO: Escape HTML special characters + let entries: Vec<&Entry> = row.entries.iter().filter(|e| e.col == col).collect(); + let class = if entries.is_empty() { "" } else { "yes" }; + let all_empty = entries.iter().all(|e| e.instance.is_none()); + let contents = if entries.is_empty() || (all_empty && entries.len() == 1) { + String::new() + } else if all_empty { + format!("{}", entries.len()) + } else { + entries + .iter() + .map(|i| render_instance(i)) + .collect::() + }; + format!("{}", contents.trim()) +} + +fn render_row(columns: &[String], row: &RowInput) -> String { + // This is O(n^2) & doesn't need to be + // TODO: Escape HTML special characters + format!( + "{}{}\n", + row.label, + &columns + .iter() + .map(|col| render_cell(col, row)) + .collect::() + ) +} + /// # Errors /// /// Will return `Err` if @@ -156,8 +194,13 @@ fn column_order(rows: &[RowInput]) -> Vec { /// * an indented line with no preceding non-indented line pub fn tablify(input: impl std::io::Read) -> Result { let rows = read_rows(input).collect::, _>>()?; - let _columns = column_order(&rows); - Ok(String::from(HEADER) + "Hello, world!" + FOOTER) + let columns = column_order(&rows); + Ok(String::from(HEADER) + + &rows + .into_iter() + .map(|r| render_row(&columns, &r)) + .collect::() + + FOOTER) } #[cfg(test)] @@ -318,4 +361,68 @@ mod tests { vec![(1, String::from("bar")), (2, String::from("baz"))] ); } + + #[test] + fn test_render_cell() { + assert_eq!( + render_cell( + "foo", + &RowInput { + label: String::from("nope"), + entries: vec![] + } + ), + String::from("") + ); + assert_eq!( + render_cell( + "foo", + &RowInput { + label: String::from("nope"), + entries: vec![Entry::from("bar")] + } + ), + String::from("") + ); + assert_eq!( + render_cell( + "foo", + &RowInput { + label: String::from("nope"), + entries: vec![Entry::from("foo")] + } + ), + String::from("") + ); + assert_eq!( + render_cell( + "foo", + &RowInput { + label: String::from("nope"), + entries: vec![Entry::from("foo"), Entry::from("foo")] + } + ), + String::from("2") + ); + assert_eq!( + render_cell( + "foo", + &RowInput { + label: String::from("nope"), + entries: vec![Entry::from("foo: 5"), Entry::from("foo: 10")] + } + ), + String::from("5 10") + ); + assert_eq!( + render_cell( + "foo", + &RowInput { + label: String::from("nope"), + entries: vec![Entry::from("foo: 5"), Entry::from("foo")] + } + ), + String::from("5 ✓") + ); + } } -- 2.50.1 From 76638ea1debd36a19c5702d22cc50b1e79f44570 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 13:46:51 -0700 Subject: [PATCH 021/100] Render column headers --- src/lib.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9504c7f..5de1c12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,8 @@ const HEADER: &str = " - "; + +"; const FOOTER: &str = "
@@ -186,6 +187,16 @@ fn render_row(columns: &[String], row: &RowInput) -> String { ) } +fn render_column_headers(columns: &[String]) -> String { + // TODO: Escape HTML special characters + String::from("") + + &columns + .iter() + .map(|c| format!("{c}")) + .collect::() + + "\n" +} + /// # Errors /// /// Will return `Err` if @@ -196,6 +207,7 @@ pub fn tablify(input: impl std::io::Read) -> Result { let rows = read_rows(input).collect::, _>>()?; let columns = column_order(&rows); Ok(String::from(HEADER) + + &render_column_headers(&columns) + &rows .into_iter() .map(|r| render_row(&columns, &r)) -- 2.50.1 From 7067975b593ef6a8b639f61dda710b023ec26a25 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 13:56:55 -0700 Subject: [PATCH 022/100] Clippy wants this to use mut and write! --- src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5de1c12..a3878c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::fmt::Write; use std::io::BufRead; use std::iter::Iterator; @@ -190,10 +191,10 @@ fn render_row(columns: &[String], row: &RowInput) -> String { fn render_column_headers(columns: &[String]) -> String { // TODO: Escape HTML special characters String::from("") - + &columns - .iter() - .map(|c| format!("{c}")) - .collect::() + + &columns.iter().fold(String::new(), |mut acc, c| { + write!(&mut acc, "{c}").unwrap(); + acc + }) + "\n" } -- 2.50.1 From 1dda21e604ca2fb9631d8f0cb56b4a82382cfeee Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 14:15:33 -0700 Subject: [PATCH 023/100] Lighter gray background for active cells --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a3878c5..1aee978 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ const HEADER: &str = " tr.key > th > div > div { width: 5em; transform-origin: bottom left; transform: translateX(1em) rotate(-65deg) } td { border: thin solid gray; } td.numeric { text-align: right; } - td.yes { border: thin solid gray; background-color: gray; } + td.yes { border: thin solid gray; background-color: #ddd; } td.spacer { border: none; } /* h/t https://stackoverflow.com/questions/5687035/css-bolding-some-text-without-changing-its-containers-size/46452396#46452396 */ .highlight { text-shadow: -0.06ex 0 black, 0.06ex 0 black; } -- 2.50.1 From 92476edc57b23a42c640f55f3a00b69bbee809b5 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 14:16:42 -0700 Subject: [PATCH 024/100] Set ids, add divs & triggers to engage with the CSS & JS --- src/lib.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1aee978..2e9bfcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,6 +159,7 @@ fn render_instance(entry: &Entry) -> String { fn render_cell(col: &str, row: &RowInput) -> String { // TODO: Escape HTML special characters + let row_label = &row.label; let entries: Vec<&Entry> = row.entries.iter().filter(|e| e.col == col).collect(); let class = if entries.is_empty() { "" } else { "yes" }; let all_empty = entries.iter().all(|e| e.instance.is_none()); @@ -172,15 +173,15 @@ fn render_cell(col: &str, row: &RowInput) -> String { .map(|i| render_instance(i)) .collect::() }; - format!("{}", contents.trim()) + format!("{}", contents.trim()) } fn render_row(columns: &[String], row: &RowInput) -> String { // This is O(n^2) & doesn't need to be // TODO: Escape HTML special characters + let row_label = &row.label; format!( - "{}{}\n", - row.label, + "{row_label}{}\n", &columns .iter() .map(|col| render_cell(col, row)) @@ -190,12 +191,12 @@ fn render_row(columns: &[String], row: &RowInput) -> String { fn render_column_headers(columns: &[String]) -> String { // TODO: Escape HTML special characters - String::from("") + String::from("") + &columns.iter().fold(String::new(), |mut acc, c| { - write!(&mut acc, "{c}").unwrap(); + write!(&mut acc, "
{c}
").unwrap(); acc }) - + "\n" + + "\n" } /// # Errors @@ -385,7 +386,7 @@ mod tests { entries: vec![] } ), - String::from("") + String::from("") ); assert_eq!( render_cell( @@ -395,7 +396,7 @@ mod tests { entries: vec![Entry::from("bar")] } ), - String::from("") + String::from("") ); assert_eq!( render_cell( @@ -405,7 +406,7 @@ mod tests { entries: vec![Entry::from("foo")] } ), - String::from("") + String::from("") ); assert_eq!( render_cell( @@ -415,7 +416,7 @@ mod tests { entries: vec![Entry::from("foo"), Entry::from("foo")] } ), - String::from("2") + String::from("2") ); assert_eq!( render_cell( @@ -425,7 +426,7 @@ mod tests { entries: vec![Entry::from("foo: 5"), Entry::from("foo: 10")] } ), - String::from("5 10") + String::from("5 10") ); assert_eq!( render_cell( @@ -435,7 +436,7 @@ mod tests { entries: vec![Entry::from("foo: 5"), Entry::from("foo")] } ), - String::from("5 ✓") + String::from("5 ✓") ); } } -- 2.50.1 From e07fd21f8e0b26c2dbaa3b4b1269b0c0a3c933c6 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 19 Aug 2024 14:23:26 -0700 Subject: [PATCH 025/100] Remove unused CSS --- src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2e9bfcc..efdb7d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,12 +17,9 @@ const HEADER: &str = " tr.key > th > div { width: 1em; } tr.key > th > div > div { width: 5em; transform-origin: bottom left; transform: translateX(1em) rotate(-65deg) } td { border: thin solid gray; } - td.numeric { text-align: right; } td.yes { border: thin solid gray; background-color: #ddd; } - td.spacer { border: none; } /* h/t https://stackoverflow.com/questions/5687035/css-bolding-some-text-without-changing-its-containers-size/46452396#46452396 */ .highlight { text-shadow: -0.06ex 0 black, 0.06ex 0 black; } - img { height: 1.2em; } @@ -35,7 +35,7 @@ const HEADER: &str = " -"; +"#; const FOOTER: &str = "
@@ -219,7 +219,9 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { ) }; row.entries.remove(col); - HTML(format!("{contents}")) + HTML(format!( + r#"{contents}"# + )) } fn render_row(columns: &[String], row: &mut Row) -> HTML { @@ -235,12 +237,12 @@ fn render_row(columns: &[String], row: &mut Row) -> HTML { fn render_column_headers(columns: &[String]) -> HTML { HTML( - String::from("") + String::from(r#""#) + &columns.iter().fold(String::new(), |mut acc, col| { let col_header = HTML::escape(col.as_ref()); write!( &mut acc, - "
{col_header}
" + r#"
{col_header}
"# ) .unwrap(); acc @@ -443,7 +445,9 @@ mod tests { entries: HashMap::new(), } ), - HTML::from("") + HTML::from( + r#""# + ) ); assert_eq!( render_cell( @@ -453,7 +457,9 @@ mod tests { entries: HashMap::from([("bar".to_owned(), vec![None])]), } ), - HTML::from("") + HTML::from( + r#""# + ) ); assert_eq!( render_cell( @@ -463,7 +469,9 @@ mod tests { entries: HashMap::from([("foo".to_owned(), vec![None])]), } ), - HTML::from("") + HTML::from( + r#""# + ) ); assert_eq!( render_cell( @@ -473,17 +481,24 @@ mod tests { entries: HashMap::from([("foo".to_owned(), vec![None, None])]), } ), - HTML::from("2") + HTML::from( + r#"2"# + ) ); assert_eq!( render_cell( "foo", &mut Row { label: "nope".to_owned(), - entries: HashMap::from([("foo".to_owned(), vec![Some("5".to_owned()), Some("10".to_owned())])]), + entries: HashMap::from([( + "foo".to_owned(), + vec![Some("5".to_owned()), Some("10".to_owned())] + )]), } ), - HTML::from("5 10") + HTML::from( + r#"5 10"# + ) ); assert_eq!( render_cell( @@ -493,7 +508,9 @@ mod tests { entries: HashMap::from([("foo".to_owned(), vec![Some("5".to_owned()), None])]), } ), - HTML::from("5 ✓") + HTML::from( + r#"5 ✓"# + ) ); assert_eq!( render_cell( @@ -503,7 +520,9 @@ mod tests { entries: HashMap::from([("heart".to_owned(), vec![Some("<3".to_owned())])]), } ), - HTML::from("<3") + HTML::from( + r#"<3"# + ) ); assert_eq!( render_cell( @@ -513,7 +532,9 @@ mod tests { entries: HashMap::from([("foo".to_owned(), vec![None])]), } ), - HTML::from("") + HTML::from( + r#""# + ) ); let mut r = Row { label: "nope".to_owned(), -- 2.50.1 From 25fd008e11cc44e03ddf0ac12da513c27bfe7a95 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 24 Sep 2024 22:40:19 -0700 Subject: [PATCH 043/100] render_row() test --- src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index cfaeb4b..47e1d3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -551,4 +551,21 @@ mod tests { render_cell("baz", &mut r); assert_eq!(r.entries.len(), 0); } + + #[test] + fn test_render_row() { + assert_eq!( + render_row( + &["foo".to_owned()], + &mut Row { + label: "nope".to_owned(), + entries: HashMap::from([("bar".to_owned(), vec![None])]), + } + ), + HTML::from( + r#"nope +"# + ) + ); + } } -- 2.50.1 From 74bd4cd1e2035284040bbaf894c271423b7a5455 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 17:43:45 -0700 Subject: [PATCH 044/100] in render_row: name 'cells' --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 47e1d3a..357a7ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,12 +226,12 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { fn render_row(columns: &[String], row: &mut Row) -> HTML { let row_label = HTML::escape(row.label.as_ref()); + let cells = columns + .iter() + .map(|col| render_cell(col, row)) + .collect::(); HTML(format!( - "{row_label}{}\n", - &columns - .iter() - .map(|col| render_cell(col, row)) - .collect::() + "{row_label}{cells}\n" )) } -- 2.50.1 From 58c0a717743d262025e664eb6ce79ad9cd4c7968 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 18:03:09 -0700 Subject: [PATCH 045/100] =?utf8?q?Rename=20render=5Finstance=20=E2=86=92?= =?utf8?q?=20render=5Fone=5Finstance?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 357a7ee..f1da5f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -187,7 +187,7 @@ fn column_order(rows: &[Row]) -> Vec { .collect() } -fn render_instance(instance: &Option) -> HTML { +fn render_one_instance(instance: &Option) -> HTML { match instance { None => HTML::from("✓"), Some(instance) => HTML::escape(instance.as_ref()), @@ -212,7 +212,7 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { instances .unwrap() .iter() - .map(render_instance) + .map(render_one_instance) .map(|html| html.0) // Waiting for slice_concat_trait to stabilize .collect::>() .join(" "), -- 2.50.1 From f915bc906e6b8001bc748a1a5f3b13fafadcb86b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 18:16:23 -0700 Subject: [PATCH 046/100] extract render_instances() --- src/lib.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f1da5f7..1396c9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -194,29 +194,32 @@ fn render_one_instance(instance: &Option) -> HTML { } } -fn render_cell(col: &str, row: &mut Row) -> HTML { - let row_label = HTML::escape(row.label.as_ref()); - let col_label = HTML::escape(col); - let instances: Option<&Vec>> = row.entries.get(col); - let class = HTML::from(if instances.is_none() { "" } else { "yes" }); - let all_empty = instances - .iter() - .flat_map(|is| is.iter()) - .all(Option::is_none); - let contents = if instances.is_none() || (all_empty && instances.unwrap().len() == 1) { +fn render_instances(instances: &[Option]) -> HTML { + let all_empty = instances.iter().all(Option::is_none); + if all_empty && instances.len() == 1 { HTML::from("") } else if all_empty { - HTML(format!("{}", instances.unwrap().len())) + HTML(format!("{}", instances.len())) } else { HTML( instances - .unwrap() .iter() .map(render_one_instance) .map(|html| html.0) // Waiting for slice_concat_trait to stabilize .collect::>() .join(" "), ) + } +} + +fn render_cell(col: &str, row: &mut Row) -> HTML { + let row_label = HTML::escape(row.label.as_ref()); + let col_label = HTML::escape(col); + let instances: Option<&Vec>> = row.entries.get(col); + let class = HTML::from(if instances.is_none() { "" } else { "yes" }); + let contents = match instances { + None => HTML::from(""), + Some(is) => render_instances(is), }; row.entries.remove(col); HTML(format!( -- 2.50.1 From 9a6260204a9a4bfa4254719c30710ea827212877 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 18:35:04 -0700 Subject: [PATCH 047/100] Leftovers column --- src/lib.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1396c9b..bad71d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -227,14 +227,38 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { )) } +fn render_leftover(notcol: &str, instances: &[Option]) -> HTML { + let label = HTML::escape(notcol); + let rest = render_instances(instances); + if rest == HTML::from("") { + HTML(format!("{label}")) + } else { + HTML(format!("{label}: {rest}")) + } +} + +fn render_all_leftovers(row: &Row) -> HTML { + let mut order: Vec<_> = row.entries.keys().collect(); + order.sort_unstable(); + HTML( + order + .into_iter() + .map(|notcol| render_leftover(notcol, row.entries.get(notcol).expect("Key vanished?!"))) + .map(|html| html.0) // Waiting for slice_concat_trait to stabilize + .collect::>() + .join(", "), + ) +} + fn render_row(columns: &[String], row: &mut Row) -> HTML { let row_label = HTML::escape(row.label.as_ref()); let cells = columns .iter() .map(|col| render_cell(col, row)) .collect::(); + let leftovers = render_all_leftovers(row); HTML(format!( - "{row_label}{cells}\n" + "{row_label}{cells}{leftovers}\n" )) } @@ -555,6 +579,37 @@ mod tests { assert_eq!(r.entries.len(), 0); } + #[test] + fn test_render_leftovers() { + assert_eq!( + render_all_leftovers(&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]) + ]), + }), + 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]) + ]), + }), + HTML::from("bar: 2, foo") + ); + } + #[test] fn test_render_row() { assert_eq!( @@ -566,7 +621,7 @@ mod tests { } ), HTML::from( - r#"nope + r#"nopebar "# ) ); -- 2.50.1 From 31af9aac0a32a20cf4111a64af65871b188bcd72 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 18:36:50 -0700 Subject: [PATCH 048/100] Don't allocate columns to rare events --- Changelog | 1 + src/lib.rs | 10 ++++++---- src/main.rs | 8 +++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Changelog b/Changelog index 33d7162..6f09172 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,7 @@ - Center text in each cell - Escape HTML characters properly - Fix an unnecessary O(n^2)ism +- Rare events appear as end-of-line notes rather than mostly-empty columns ## [0.2.1] - 2024-08-20 - A little more space up top diff --git a/src/lib.rs b/src/lib.rs index bad71d0..a7c2819 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,9 @@ use std::fmt::Write; use std::io::BufRead; use std::iter::Iterator; -pub struct Config {} +pub struct Config { + pub column_threshold: usize, +} const HEADER: &str = r#" @@ -180,10 +182,10 @@ fn column_counts(rows: &[Row]) -> Vec<(usize, String)> { counts.sort_unstable_by(|(an, acol), (bn, bcol)| bn.cmp(an).then(acol.cmp(bcol))); counts } -fn column_order(rows: &[Row]) -> Vec { +fn column_order(config: &Config, rows: &[Row]) -> Vec { column_counts(rows) .into_iter() - .map(|(_, col)| col) + .filter_map(|(n, col)| (n >= config.column_threshold).then_some(col)) .collect() } @@ -286,7 +288,7 @@ fn render_column_headers(columns: &[String]) -> HTML { /// * an indented line with no preceding non-indented line pub fn tablify(config: &Config, input: impl std::io::Read) -> Result { let rows = read_rows(input).collect::, _>>()?; - let columns = column_order(&rows); + let columns = column_order(config, &rows); Ok(HTML(format!( "{HEADER}{}{}{FOOTER}", render_column_headers(&columns), diff --git a/src/main.rs b/src/main.rs index b4f32fe..6102247 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,12 @@ fn main() { print!( "{}", - tablify::tablify(&tablify::Config {}, std::io::stdin()).unwrap() + tablify::tablify( + &tablify::Config { + column_threshold: 2 + }, + std::io::stdin() + ) + .unwrap() ); } -- 2.50.1 From 36bc3a39ef440cc1af77d7d88844cacbe6b4387d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 18:45:08 -0700 Subject: [PATCH 049/100] Tweak appearance of leftovers column --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a7c2819..cbe9ae9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ const HEADER: &str = r#" tr.key > th > div { width: 1em; } tr.key > th > div > div { width: 5em; transform-origin: bottom left; transform: translateX(1em) rotate(-65deg) } td { border: thin solid gray; } + td.leftover { text-align: left; border: none; padding-left: .4em; } td.yes { border: thin solid gray; background-color: #ddd; } /* h/t https://stackoverflow.com/questions/5687035/css-bolding-some-text-without-changing-its-containers-size/46452396#46452396 */ .highlight { text-shadow: -0.06ex 0 black, 0.06ex 0 black; } @@ -260,7 +261,7 @@ fn render_row(columns: &[String], row: &mut Row) -> HTML { .collect::(); let leftovers = render_all_leftovers(row); HTML(format!( - "{row_label}{cells}{leftovers}\n" + "{row_label}{cells}{leftovers}\n" )) } @@ -623,7 +624,7 @@ mod tests { } ), HTML::from( - r#"nopebar + r#"nopebar "# ) ); -- 2.50.1 From 06a6a5cad195d0b7504c4e7b4caf085487f53578 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 20:44:53 -0700 Subject: [PATCH 050/100] Rowlike: Allow spacer rows to be represented --- src/lib.rs | 99 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index cbe9ae9..73e517b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,12 @@ struct Row { entries: HashMap>>, } +#[derive(Debug, PartialEq, Eq)] +enum Rowlike { + Row(Row), + Spacer, +} + struct Reader>> { input: std::iter::Enumerate, row: Option, @@ -122,15 +128,15 @@ impl>> Reader { } } impl>> Iterator for Reader { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { loop { match self.input.next() { - None => return Ok(std::mem::take(&mut self.row)).transpose(), + None => return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose(), Some((_, Err(e))) => return Some(Err(e)), Some((n, Ok(line))) => match InputLine::from(line.as_ref()) { InputLine::Blank if self.row.is_some() => { - return Ok(std::mem::take(&mut self.row)).transpose() + return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose() } InputLine::Blank => {} InputLine::Entry(col, instance) => match &mut self.row { @@ -154,7 +160,7 @@ impl>> Iterator for Reader entries: HashMap::new(), }); if prev.is_some() { - return Ok(prev).transpose(); + return Ok(prev.map(Rowlike::Row)).transpose(); } } }, @@ -163,14 +169,18 @@ impl>> Iterator for Reader } } -fn read_rows(input: impl std::io::Read) -> impl Iterator> { +fn read_rows(input: impl std::io::Read) -> impl Iterator> { Reader::new(std::io::BufReader::new(input).lines()) } -fn column_counts(rows: &[Row]) -> Vec<(usize, String)> { +fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { + let empty = HashMap::new(); let mut counts: Vec<_> = rows .iter() - .flat_map(|r| r.entries.keys()) + .flat_map(|rl| match rl { + Rowlike::Row(r) => r.entries.keys(), + Rowlike::Spacer => empty.keys(), + }) .fold(HashMap::new(), |mut cs, col| { cs.entry(col.to_owned()) .and_modify(|n| *n += 1) @@ -183,7 +193,7 @@ fn column_counts(rows: &[Row]) -> Vec<(usize, String)> { counts.sort_unstable_by(|(an, acol), (bn, bcol)| bn.cmp(an).then(acol.cmp(bcol))); counts } -fn column_order(config: &Config, rows: &[Row]) -> Vec { +fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { column_counts(rows) .into_iter() .filter_map(|(n, col)| (n >= config.column_threshold).then_some(col)) @@ -253,16 +263,21 @@ fn render_all_leftovers(row: &Row) -> HTML { ) } -fn render_row(columns: &[String], row: &mut Row) -> HTML { - let row_label = HTML::escape(row.label.as_ref()); - let cells = columns - .iter() - .map(|col| render_cell(col, row)) - .collect::(); - let leftovers = render_all_leftovers(row); - HTML(format!( - "{row_label}{cells}{leftovers}\n" - )) +fn render_row(columns: &[String], rowlike: &mut Rowlike) -> HTML { + match rowlike { + Rowlike::Spacer => HTML::from(" "), + Rowlike::Row(row) => { + let row_label = HTML::escape(row.label.as_ref()); + let cells = columns + .iter() + .map(|col| render_cell(col, row)) + .collect::(); + let leftovers = render_all_leftovers(row); + HTML(format!( + "{row_label}{cells}{leftovers}\n" + )) + } + } } fn render_column_headers(columns: &[String]) -> HTML { @@ -336,63 +351,63 @@ mod tests { fn test_read_rows() { assert_eq!( read_rows(&b"foo"[..]).flatten().collect::>(), - vec![Row { + vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::new(), - }] + })] ); assert_eq!( read_rows(&b"bar"[..]).flatten().collect::>(), - vec![Row { + vec![Rowlike::Row(Row { label: "bar".to_owned(), entries: HashMap::new(), - }] + })] ); assert_eq!( read_rows(&b"foo\nbar\n"[..]).flatten().collect::>(), vec![ - Row { + Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::new(), - }, - Row { + }), + Rowlike::Row(Row { label: "bar".to_owned(), entries: HashMap::new(), - } + }) ] ); assert_eq!( read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), - vec![Row { + vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::from([("bar".to_owned(), vec![None])]), - }] + })] ); assert_eq!( read_rows(&b"foo\n bar\n baz\n"[..]) .flatten() .collect::>(), - vec![Row { + vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::from([ ("bar".to_owned(), vec![None]), ("baz".to_owned(), vec![None]) ]), - }] + })] ); assert_eq!( read_rows(&b"foo\n\nbar\n"[..]) .flatten() .collect::>(), vec![ - Row { + Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::new(), - }, - Row { + }), + Rowlike::Row(Row { label: "bar".to_owned(), entries: HashMap::new(), - } + }) ] ); assert_eq!( @@ -400,24 +415,24 @@ mod tests { .flatten() .collect::>(), vec![ - Row { + Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::new(), - }, - Row { + }), + Rowlike::Row(Row { label: "bar".to_owned(), entries: HashMap::new(), - } + }) ] ); assert_eq!( read_rows(&b"foo \n bar \n"[..]) .flatten() .collect::>(), - vec![Row { + vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::from([("bar".to_owned(), vec![None])]), - }] + })] ); let bad = read_rows(&b" foo"[..]).next().unwrap(); @@ -618,10 +633,10 @@ mod tests { assert_eq!( render_row( &["foo".to_owned()], - &mut Row { + &mut Rowlike::Row(Row { label: "nope".to_owned(), entries: HashMap::from([("bar".to_owned(), vec![None])]), - } + }) ), HTML::from( r#"nopebar -- 2.50.1 From 722ea29747d6e820490db0bf988cda86c3341565 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 20:52:08 -0700 Subject: [PATCH 051/100] Verify double-line-skip behavior without spacer rows --- src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 73e517b..e11f08b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -410,6 +410,21 @@ mod tests { }) ] ); + assert_eq!( + read_rows(&b"foo\n\n\nbar\n"[..]) + .flatten() + .collect::>(), + vec![ + Rowlike::Row(Row { + label: "foo".to_owned(), + entries: HashMap::new(), + }), + Rowlike::Row(Row { + label: "bar".to_owned(), + entries: HashMap::new(), + }) + ] + ); assert_eq!( read_rows(&b"foo\n \nbar\n"[..]) .flatten() -- 2.50.1 From 14a039db4c850c41ca1cd72c692afe59d6d1347b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 20:56:52 -0700 Subject: [PATCH 052/100] Double blank lines in the input generate spacer rows --- Changelog | 1 + src/lib.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 6f09172..7158f63 100644 --- a/Changelog +++ b/Changelog @@ -3,6 +3,7 @@ - Escape HTML characters properly - Fix an unnecessary O(n^2)ism - Rare events appear as end-of-line notes rather than mostly-empty columns +- Double blank line for small break between rows ## [0.2.1] - 2024-08-20 - A little more space up top diff --git a/src/lib.rs b/src/lib.rs index e11f08b..744dc66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,7 +138,7 @@ impl>> Iterator for Reader InputLine::Blank if self.row.is_some() => { return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose() } - InputLine::Blank => {} + InputLine::Blank => return Some(Ok(Rowlike::Spacer)), InputLine::Entry(col, instance) => match &mut self.row { None => { return Some(Err(std::io::Error::other(format!( @@ -419,6 +419,7 @@ mod tests { label: "foo".to_owned(), entries: HashMap::new(), }), + Rowlike::Spacer, Rowlike::Row(Row { label: "bar".to_owned(), entries: HashMap::new(), -- 2.50.1 From 529cbaa2f61a4318ea6e6982f7ff08c44cbcf1ab Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 25 Sep 2024 21:14:12 -0700 Subject: [PATCH 053/100] Tweak appearance of spacer rows --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 744dc66..fbadb2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ const HEADER: &str = r#" /* h/t https://wabain.github.io/2019/10/13/css-rotated-table-header.html */ th, td { white-space: nowrap; } th { text-align: left; font-weight: normal; } + th.spacer_row { height: .3em; } table { border-collapse: collapse } tr.key > th { height: 10em; vertical-align: bottom; line-height: 1 } tr.key > th > div { width: 1em; } @@ -265,7 +266,7 @@ fn render_all_leftovers(row: &Row) -> HTML { fn render_row(columns: &[String], rowlike: &mut Rowlike) -> HTML { match rowlike { - Rowlike::Spacer => HTML::from(" "), + Rowlike::Spacer => HTML::from("\n"), Rowlike::Row(row) => { let row_label = HTML::escape(row.label.as_ref()); let cells = columns -- 2.50.1 From 12e913005080a9f60750d807c220a58704429361 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 2 Oct 2024 02:25:40 -0700 Subject: [PATCH 054/100] Make read_rows() strict (not lazy) --- src/lib.rs | 60 ++++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fbadb2a..dbcfc7b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,8 +170,8 @@ impl>> Iterator for Reader } } -fn read_rows(input: impl std::io::Read) -> impl Iterator> { - Reader::new(std::io::BufReader::new(input).lines()) +fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { + Reader::new(std::io::BufReader::new(input).lines()).collect::, _>>() } fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { @@ -304,7 +304,7 @@ fn render_column_headers(columns: &[String]) -> HTML { /// * the log has invalid syntax: /// * an indented line with no preceding non-indented line pub fn tablify(config: &Config, input: impl std::io::Read) -> Result { - let rows = read_rows(input).collect::, _>>()?; + let rows = read_rows(input)?; let columns = column_order(config, &rows); Ok(HTML(format!( "{HEADER}{}{}{FOOTER}", @@ -351,21 +351,21 @@ mod tests { #[test] fn test_read_rows() { assert_eq!( - read_rows(&b"foo"[..]).flatten().collect::>(), + read_rows(&b"foo"[..]).unwrap(), vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::new(), })] ); assert_eq!( - read_rows(&b"bar"[..]).flatten().collect::>(), + read_rows(&b"bar"[..]).unwrap(), vec![Rowlike::Row(Row { label: "bar".to_owned(), entries: HashMap::new(), })] ); assert_eq!( - read_rows(&b"foo\nbar\n"[..]).flatten().collect::>(), + read_rows(&b"foo\nbar\n"[..]).unwrap(), vec![ Rowlike::Row(Row { label: "foo".to_owned(), @@ -378,16 +378,14 @@ mod tests { ] ); assert_eq!( - read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), + read_rows(&b"foo\n bar\n"[..]).unwrap(), vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::from([("bar".to_owned(), vec![None])]), })] ); assert_eq!( - read_rows(&b"foo\n bar\n baz\n"[..]) - .flatten() - .collect::>(), + read_rows(&b"foo\n bar\n baz\n"[..]).unwrap(), vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::from([ @@ -397,9 +395,7 @@ mod tests { })] ); assert_eq!( - read_rows(&b"foo\n\nbar\n"[..]) - .flatten() - .collect::>(), + read_rows(&b"foo\n\nbar\n"[..]).unwrap(), vec![ Rowlike::Row(Row { label: "foo".to_owned(), @@ -412,9 +408,7 @@ mod tests { ] ); assert_eq!( - read_rows(&b"foo\n\n\nbar\n"[..]) - .flatten() - .collect::>(), + read_rows(&b"foo\n\n\nbar\n"[..]).unwrap(), vec![ Rowlike::Row(Row { label: "foo".to_owned(), @@ -428,9 +422,7 @@ mod tests { ] ); assert_eq!( - read_rows(&b"foo\n \nbar\n"[..]) - .flatten() - .collect::>(), + read_rows(&b"foo\n \nbar\n"[..]).unwrap(), vec![ Rowlike::Row(Row { label: "foo".to_owned(), @@ -443,20 +435,18 @@ mod tests { ] ); assert_eq!( - read_rows(&b"foo \n bar \n"[..]) - .flatten() - .collect::>(), + read_rows(&b"foo \n bar \n"[..]).unwrap(), vec![Rowlike::Row(Row { label: "foo".to_owned(), entries: HashMap::from([("bar".to_owned(), vec![None])]), })] ); - let bad = read_rows(&b" foo"[..]).next().unwrap(); + let bad = read_rows(&b" foo"[..]); assert!(bad.is_err()); assert!(format!("{bad:?}").contains("1: Entry with no header")); - let bad2 = read_rows(&b"foo\n\n bar"[..]).nth(1).unwrap(); + let bad2 = read_rows(&b"foo\n\n bar"[..]); assert!(bad2.is_err()); assert!(format!("{bad2:?}").contains("3: Entry with no header")); } @@ -464,34 +454,20 @@ mod tests { #[test] fn test_column_counts() { assert_eq!( - column_counts( - &read_rows(&b"foo\n bar\n baz\n"[..]) - .collect::, _>>() - .unwrap() - ), + column_counts(&read_rows(&b"foo\n bar\n baz\n"[..]).unwrap()), vec![(1, String::from("bar")), (1, String::from("baz"))] ); assert_eq!( - column_counts( - &read_rows(&b"foo\n bar\n baz\nquux\n baz"[..]) - .collect::, _>>() - .unwrap() - ), + column_counts(&read_rows(&b"foo\n bar\n baz\nquux\n baz"[..]).unwrap()), vec![(2, String::from("baz")), (1, String::from("bar"))] ); assert_eq!( - column_counts( - &read_rows(&b"foo\n bar\n bar\n baz\n bar\nquux\n baz"[..]) - .collect::, _>>() - .unwrap() - ), + column_counts(&read_rows(&b"foo\n bar\n bar\n baz\n bar\nquux\n baz"[..]).unwrap()), vec![(2, String::from("baz")), (1, String::from("bar"))] ); assert_eq!( column_counts( - &read_rows(&b"foo\n bar: 1\n bar: 2\n baz\n bar\nquux\n baz"[..]) - .collect::, _>>() - .unwrap() + &read_rows(&b"foo\n bar: 1\n bar: 2\n baz\n bar\nquux\n baz"[..]).unwrap() ), vec![(2, String::from("baz")), (1, String::from("bar"))] ); -- 2.50.1 From 586b332ada9b821cbc7838ffa9151c9886387918 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 2 Oct 2024 02:32:34 -0700 Subject: [PATCH 055/100] Get config from read_input() --- src/lib.rs | 18 +++++++++++++----- src/main.rs | 11 +---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dbcfc7b..5c994c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,8 +170,13 @@ impl>> Iterator for Reader } } -fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { - Reader::new(std::io::BufReader::new(input).lines()).collect::, _>>() +fn read_input(input: impl std::io::Read) -> Result<(Vec, Config), std::io::Error> { + let default_config = Config { + column_threshold: 2, + }; + Reader::new(std::io::BufReader::new(input).lines()) + .collect::, _>>() + .map(|rows| (rows, default_config)) } fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { @@ -303,9 +308,9 @@ fn render_column_headers(columns: &[String]) -> HTML { /// * there's an i/o error while reading `input` /// * the log has invalid syntax: /// * an indented line with no preceding non-indented line -pub fn tablify(config: &Config, input: impl std::io::Read) -> Result { - let rows = read_rows(input)?; - let columns = column_order(config, &rows); +pub fn tablify(input: impl std::io::Read) -> Result { + let (rows, config) = read_input(input)?; + let columns = column_order(&config, &rows); Ok(HTML(format!( "{HEADER}{}{}{FOOTER}", render_column_headers(&columns), @@ -348,6 +353,9 @@ mod tests { ); } + fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { + read_input(input).map(|(rows, _)| rows) + } #[test] fn test_read_rows() { assert_eq!( diff --git a/src/main.rs b/src/main.rs index 6102247..8cf4859 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,3 @@ fn main() { - print!( - "{}", - tablify::tablify( - &tablify::Config { - column_threshold: 2 - }, - std::io::stdin() - ) - .unwrap() - ); + print!("{}", tablify::tablify(std::io::stdin()).unwrap()); } -- 2.50.1 From 166a543d8299317183ec9226c3af174a6d4f4fc9 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 2 Oct 2024 02:45:42 -0700 Subject: [PATCH 056/100] Release 0.3.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- Changelog | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c73dc3..24c5412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.3.0-pre" +version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 0b0c6f2..31c8756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.3.0-pre" +version = "0.3.0" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" diff --git a/Changelog b/Changelog index 7158f63..2bf1261 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## [Unreleased] + +## [0.3.0] - 2024-10-02 - Center text in each cell - Escape HTML characters properly - Fix an unnecessary O(n^2)ism -- 2.50.1 From 5a070b309feffc87f170365411c5e71626f72520 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 22:09:08 -0700 Subject: [PATCH 057/100] Start on 0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24c5412..5ccde79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.3.0" +version = "0.4.0-pre" diff --git a/Cargo.toml b/Cargo.toml index 31c8756..d8ab194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.3.0" +version = "0.4.0-pre" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" -- 2.50.1 From e44de444711dd8dc9d79f4fd6ab86c2d0fd26862 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 2 Oct 2024 14:15:46 -0700 Subject: [PATCH 058/100] Read column threshold from `!col_threshold ` in input --- Changelog | 1 + src/lib.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Changelog b/Changelog index 2bf1261..cf5af53 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ ## [Unreleased] +- Read column threshold from `!col_threshold ` in input ## [0.3.0] - 2024-10-02 - Center text in each cell diff --git a/src/lib.rs b/src/lib.rs index 5c994c9..b52806a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,19 @@ use std::fmt::Write; use std::io::BufRead; use std::iter::Iterator; -pub struct Config { - pub column_threshold: usize, +#[derive(PartialEq, Eq, Debug)] +struct Config { + column_threshold: usize, +} +impl Config { + fn apply_command(&mut self, cmd: &str) -> Result<(), std::io::Error> { + if let Some(threshold) = cmd.strip_prefix("col_threshold ") { + self.column_threshold = threshold + .parse() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + } + Ok(()) + } } const HEADER: &str = r#" @@ -87,12 +98,15 @@ enum InputLine<'a> { Blank, RowHeader(&'a str), Entry(&'a str, Option<&'a str>), + Command(&'a str), } impl<'a> From<&'a str> for InputLine<'a> { fn from(value: &'a str) -> InputLine<'a> { let trimmed = value.trim_end(); if trimmed.is_empty() { InputLine::Blank + } else if let Some(cmd) = trimmed.strip_prefix('!') { + InputLine::Command(cmd) } else if !trimmed.starts_with(' ') { InputLine::RowHeader(value.trim()) } else { @@ -116,19 +130,23 @@ enum Rowlike { Spacer, } -struct Reader>> { +struct Reader<'cfg, Input: Iterator>> { input: std::iter::Enumerate, row: Option, + config: &'cfg mut Config, } -impl>> Reader { - fn new(input: Input) -> Self { +impl<'cfg, Input: Iterator>> Reader<'cfg, Input> { + fn new(config: &'cfg mut Config, input: Input) -> Self { Self { input: input.enumerate(), row: None, + config, } } } -impl>> Iterator for Reader { +impl<'cfg, Input: Iterator>> Iterator + for Reader<'cfg, Input> +{ type Item = Result; fn next(&mut self) -> Option { loop { @@ -136,6 +154,11 @@ impl>> Iterator for Reader None => return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose(), Some((_, Err(e))) => return Some(Err(e)), Some((n, Ok(line))) => match InputLine::from(line.as_ref()) { + InputLine::Command(cmd) => { + if let Err(e) = self.config.apply_command(cmd) { + return Some(Err(e)); + } + } InputLine::Blank if self.row.is_some() => { return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose() } @@ -171,12 +194,13 @@ impl>> Iterator for Reader } fn read_input(input: impl std::io::Read) -> Result<(Vec, Config), std::io::Error> { - let default_config = Config { + let mut config = Config { column_threshold: 2, }; - Reader::new(std::io::BufReader::new(input).lines()) + let reader = Reader::new(&mut config, std::io::BufReader::new(input).lines()); + reader .collect::, _>>() - .map(|rows| (rows, default_config)) + .map(|rows| (rows, config)) } fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { @@ -356,6 +380,9 @@ mod tests { fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { read_input(input).map(|(rows, _)| rows) } + fn read_config(input: impl std::io::Read) -> Result { + read_input(input).map(|(_, config)| config) + } #[test] fn test_read_rows() { assert_eq!( @@ -459,6 +486,20 @@ mod tests { assert!(format!("{bad2:?}").contains("3: Entry with no header")); } + #[test] + fn test_read_config() { + assert_eq!( + read_config(&b"!col_threshold 10"[..]).unwrap(), + Config { + column_threshold: 10 + } + ); + + let bad_num = read_config(&b"!col_threshold foo"[..]); + assert!(bad_num.is_err()); + assert!(format!("{bad_num:?}").contains("Parse")); + } + #[test] fn test_column_counts() { assert_eq!( -- 2.50.1 From fa8b547998d23b948cadde2c9e8f35b9055f9189 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 2 Oct 2024 21:54:33 -0700 Subject: [PATCH 059/100] Don't compare entire Config in tests --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b52806a..d55aa53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -489,10 +489,10 @@ mod tests { #[test] fn test_read_config() { assert_eq!( - read_config(&b"!col_threshold 10"[..]).unwrap(), - Config { - column_threshold: 10 - } + read_config(&b"!col_threshold 10"[..]) + .unwrap() + .column_threshold, + 10 ); let bad_num = read_config(&b"!col_threshold foo"[..]); -- 2.50.1 From 215d38d5b5ca4c77bc517c75fc93498a1d20b24b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 2 Oct 2024 22:00:39 -0700 Subject: [PATCH 060/100] Plumb config around --- src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d55aa53..acf5383 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -293,7 +293,7 @@ fn render_all_leftovers(row: &Row) -> HTML { ) } -fn render_row(columns: &[String], rowlike: &mut Rowlike) -> HTML { +fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTML { match rowlike { Rowlike::Spacer => HTML::from("\n"), Rowlike::Row(row) => { @@ -310,7 +310,7 @@ fn render_row(columns: &[String], rowlike: &mut Rowlike) -> HTML { } } -fn render_column_headers(columns: &[String]) -> HTML { +fn render_column_headers(config: &Config, columns: &[String]) -> HTML { HTML( String::from(r#""#) + &columns.iter().fold(String::new(), |mut acc, col| { @@ -337,9 +337,9 @@ pub fn tablify(input: impl std::io::Read) -> Result { let columns = column_order(&config, &rows); Ok(HTML(format!( "{HEADER}{}{}{FOOTER}", - render_column_headers(&columns), + render_column_headers(&config, &columns), rows.into_iter() - .map(|mut r| render_row(&columns, &mut r)) + .map(|mut r| render_row(&config, &columns, &mut r)) .collect::() ))) } @@ -674,6 +674,9 @@ mod tests { fn test_render_row() { assert_eq!( render_row( + &Config { + column_threshold: 0, + }, &["foo".to_owned()], &mut Rowlike::Row(Row { label: "nope".to_owned(), -- 2.50.1 From a411a19d502a2ee8ce16c0cd7f447dac3293cee4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 03:14:53 -0700 Subject: [PATCH 061/100] Allow forcing column layout with `!col ` in input --- Changelog | 1 + src/lib.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/Changelog b/Changelog index cf5af53..fbee5bd 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,6 @@ ## [Unreleased] - Read column threshold from `!col_threshold ` in input +- Allow forcing column layout with `!col ` in input ## [0.3.0] - 2024-10-02 - Center text in each cell diff --git a/src/lib.rs b/src/lib.rs index acf5383..af582d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use std::borrow::ToOwned; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::io::BufRead; use std::iter::Iterator; @@ -7,6 +7,7 @@ use std::iter::Iterator; #[derive(PartialEq, Eq, Debug)] struct Config { column_threshold: usize, + static_columns: Vec, } impl Config { fn apply_command(&mut self, cmd: &str) -> Result<(), std::io::Error> { @@ -14,6 +15,8 @@ impl Config { self.column_threshold = threshold .parse() .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + } else if let Some(col) = cmd.strip_prefix("col ") { + self.static_columns.push(col.to_owned()); } Ok(()) } @@ -196,6 +199,7 @@ impl<'cfg, Input: Iterator>> Iterator fn read_input(input: impl std::io::Read) -> Result<(Vec, Config), std::io::Error> { let mut config = Config { column_threshold: 2, + static_columns: vec![], }; let reader = Reader::new(&mut config, std::io::BufReader::new(input).lines()); reader @@ -224,9 +228,16 @@ fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { counts } fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { + let static_columns: HashSet<&str> = config + .static_columns + .iter() + .map(std::string::String::as_str) + .collect(); column_counts(rows) .into_iter() - .filter_map(|(n, col)| (n >= config.column_threshold).then_some(col)) + .filter_map(|(n, col)| { + (n >= config.column_threshold && !static_columns.contains(col.as_str())).then_some(col) + }) .collect() } @@ -298,13 +309,18 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM Rowlike::Spacer => HTML::from("\n"), Rowlike::Row(row) => { let row_label = HTML::escape(row.label.as_ref()); - let cells = columns + let static_cells = config + .static_columns + .iter() + .map(|col| render_cell(col, row)) + .collect::(); + let dynamic_cells = columns .iter() .map(|col| render_cell(col, row)) .collect::(); let leftovers = render_all_leftovers(row); HTML(format!( - "{row_label}{cells}{leftovers}\n" + "{row_label}{static_cells}{dynamic_cells}{leftovers}\n" )) } } @@ -313,15 +329,18 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM fn render_column_headers(config: &Config, columns: &[String]) -> HTML { HTML( String::from(r#""#) - + &columns.iter().fold(String::new(), |mut acc, col| { - let col_header = HTML::escape(col.as_ref()); - write!( - &mut acc, - r#"
{col_header}
"# - ) - .unwrap(); - acc - }) + + &config.static_columns.iter().chain(columns.iter()).fold( + String::new(), + |mut acc, col| { + let col_header = HTML::escape(col.as_ref()); + write!( + &mut acc, + r#"
{col_header}
"# + ) + .unwrap(); + acc + }, + ) + "\n", ) } @@ -494,6 +513,10 @@ mod tests { .column_threshold, 10 ); + assert_eq!( + read_config(&b"!col foo"[..]).unwrap().static_columns, + vec!["foo".to_owned()] + ); let bad_num = read_config(&b"!col_threshold foo"[..]); assert!(bad_num.is_err()); @@ -676,6 +699,7 @@ mod tests { render_row( &Config { column_threshold: 0, + static_columns: vec![], }, &["foo".to_owned()], &mut Rowlike::Row(Row { @@ -685,6 +709,27 @@ mod tests { ), HTML::from( r#"nopebar +"# + ) + ); + assert_eq!( + render_row( + &Config { + column_threshold: 0, + static_columns: vec!["foo".to_owned(), "bar".to_owned()], + }, + &["baz".to_owned()], + &mut Rowlike::Row(Row { + label: "nope".to_owned(), + entries: HashMap::from([ + ("bar".to_owned(), vec![Some("r".to_owned())]), + ("baz".to_owned(), vec![Some("z".to_owned())]), + ("foo".to_owned(), vec![Some("f".to_owned())]), + ]), + }) + ), + HTML::from( + r#"nopefrz "# ) ); -- 2.50.1 From b2f318323ffed782b90237e38015d65638848d3d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 14:09:41 -0700 Subject: [PATCH 062/100] Invalid/unknown commands are errors --- src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index af582d4..d974461 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,11 @@ impl Config { .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; } else if let Some(col) = cmd.strip_prefix("col ") { self.static_columns.push(col.to_owned()); + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Unknown command: {cmd}"), + )); } Ok(()) } @@ -518,6 +523,10 @@ mod tests { vec!["foo".to_owned()] ); + let bad_command = read_config(&b"!no such command"[..]); + assert!(bad_command.is_err()); + assert!(format!("{bad_command:?}").contains("Unknown command")); + let bad_num = read_config(&b"!col_threshold foo"[..]); assert!(bad_num.is_err()); assert!(format!("{bad_num:?}").contains("Parse")); -- 2.50.1 From 1df4654a9fff0f0a1e146c681dc05af7c3e460fe Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 14:12:08 -0700 Subject: [PATCH 063/100] Make it more clear that the number in an error message is a line number --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d974461..e20d23d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,7 +174,7 @@ impl<'cfg, Input: Iterator>> Iterator InputLine::Entry(col, instance) => match &mut self.row { None => { return Some(Err(std::io::Error::other(format!( - "{}: Entry with no header", + "line {}: Entry with no header", n + 1 )))) } @@ -503,11 +503,11 @@ mod tests { let bad = read_rows(&b" foo"[..]); assert!(bad.is_err()); - assert!(format!("{bad:?}").contains("1: Entry with no header")); + assert!(format!("{bad:?}").contains("line 1: Entry with no header")); let bad2 = read_rows(&b"foo\n\n bar"[..]); assert!(bad2.is_err()); - assert!(format!("{bad2:?}").contains("3: Entry with no header")); + assert!(format!("{bad2:?}").contains("line 3: Entry with no header")); } #[test] -- 2.50.1 From f105c5bc0dbbe15d36acd0d596b0e6fa7d22b1d0 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 14:17:31 -0700 Subject: [PATCH 064/100] Show line number in command-related error messages --- src/lib.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e20d23d..09e2e7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,17 +10,20 @@ struct Config { static_columns: Vec, } impl Config { - fn apply_command(&mut self, cmd: &str) -> Result<(), std::io::Error> { + fn apply_command(&mut self, line_num: usize, cmd: &str) -> Result<(), std::io::Error> { if let Some(threshold) = cmd.strip_prefix("col_threshold ") { - self.column_threshold = threshold - .parse() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + self.column_threshold = threshold.parse().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("line {line_num}: col_threshold must be numeric: {e}"), + ) + })?; } else if let Some(col) = cmd.strip_prefix("col ") { self.static_columns.push(col.to_owned()); } else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, - format!("Unknown command: {cmd}"), + format!("line {line_num}: Unknown command: {cmd}"), )); } Ok(()) @@ -163,7 +166,7 @@ impl<'cfg, Input: Iterator>> Iterator Some((_, Err(e))) => return Some(Err(e)), Some((n, Ok(line))) => match InputLine::from(line.as_ref()) { InputLine::Command(cmd) => { - if let Err(e) = self.config.apply_command(cmd) { + if let Err(e) = self.config.apply_command(n + 1, cmd) { return Some(Err(e)); } } @@ -525,11 +528,11 @@ mod tests { let bad_command = read_config(&b"!no such command"[..]); assert!(bad_command.is_err()); - assert!(format!("{bad_command:?}").contains("Unknown command")); + assert!(format!("{bad_command:?}").contains("line 1: Unknown command")); let bad_num = read_config(&b"!col_threshold foo"[..]); assert!(bad_num.is_err()); - assert!(format!("{bad_num:?}").contains("Parse")); + assert!(format!("{bad_num:?}").contains("line 1: col_threshold must be numeric")); } #[test] -- 2.50.1 From a05772014cb65edac0cc664296802036bdc48ca4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 20:48:45 -0700 Subject: [PATCH 065/100] !colsep to separate !col columns --- Changelog | 1 + src/lib.rs | 63 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Changelog b/Changelog index fbee5bd..37cde58 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,7 @@ ## [Unreleased] - Read column threshold from `!col_threshold ` in input - Allow forcing column layout with `!col ` in input +- `!colsep` to separate `!col` columns ## [0.3.0] - 2024-10-02 - Center text in each cell diff --git a/src/lib.rs b/src/lib.rs index 09e2e7a..e8a2ea0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use std::iter::Iterator; #[derive(PartialEq, Eq, Debug)] struct Config { column_threshold: usize, - static_columns: Vec, + static_columns: Vec>, } impl Config { fn apply_command(&mut self, line_num: usize, cmd: &str) -> Result<(), std::io::Error> { @@ -19,7 +19,9 @@ impl Config { ) })?; } else if let Some(col) = cmd.strip_prefix("col ") { - self.static_columns.push(col.to_owned()); + self.static_columns.push(Some(col.to_owned())); + } else if cmd == "colsep" { + self.static_columns.push(None); } else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -41,6 +43,7 @@ const HEADER: &str = r#" th, td { white-space: nowrap; } th { text-align: left; font-weight: normal; } th.spacer_row { height: .3em; } + .spacer_col { border: none; width: .2em; } table { border-collapse: collapse } tr.key > th { height: 10em; vertical-align: bottom; line-height: 1 } tr.key > th > div { width: 1em; } @@ -239,6 +242,7 @@ fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { let static_columns: HashSet<&str> = config .static_columns .iter() + .flatten() .map(std::string::String::as_str) .collect(); column_counts(rows) @@ -320,7 +324,10 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM let static_cells = config .static_columns .iter() - .map(|col| render_cell(col, row)) + .map(|ocol| match ocol { + Some(col) => render_cell(col, row), + None => HTML::from(r#""#), + }) .collect::(); let dynamic_cells = columns .iter() @@ -335,20 +342,26 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM } fn render_column_headers(config: &Config, columns: &[String]) -> HTML { + let static_columns = config.static_columns.iter().map(|oc| oc.as_ref()); + let dynamic_columns = columns.iter().map(Some); HTML( String::from(r#""#) - + &config.static_columns.iter().chain(columns.iter()).fold( - String::new(), - |mut acc, col| { - let col_header = HTML::escape(col.as_ref()); - write!( - &mut acc, - r#"
{col_header}
"# - ) + + &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#""#), + } .unwrap(); acc - }, - ) + }) + "\n", ) } @@ -523,7 +536,7 @@ mod tests { ); assert_eq!( read_config(&b"!col foo"[..]).unwrap().static_columns, - vec!["foo".to_owned()] + vec![Some("foo".to_owned())] ); let bad_command = read_config(&b"!no such command"[..]); @@ -728,7 +741,7 @@ mod tests { render_row( &Config { column_threshold: 0, - static_columns: vec!["foo".to_owned(), "bar".to_owned()], + static_columns: vec![Some("foo".to_owned()), Some("bar".to_owned())], }, &["baz".to_owned()], &mut Rowlike::Row(Row { @@ -742,6 +755,26 @@ mod tests { ), HTML::from( r#"nopefrz +"# + ) + ); + assert_eq!( + render_row( + &Config { + column_threshold: 0, + static_columns: vec![Some("foo".to_owned()), None, Some("bar".to_owned())], + }, + &[], + &mut Rowlike::Row(Row { + label: "nope".to_owned(), + entries: HashMap::from([ + ("bar".to_owned(), vec![Some("r".to_owned())]), + ("foo".to_owned(), vec![Some("f".to_owned())]), + ]), + }) + ), + HTML::from( + r#"nopefr "# ) ); -- 2.50.1 From 71e34cc04258862ab4ef7177c2719ca3ec8bedcf Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 21:33:22 -0700 Subject: [PATCH 066/100] Name the DEFAULT_CONFIG --- src/lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e8a2ea0..74a54a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,11 @@ impl Config { } } +const DEFAULT_CONFIG: Config = Config { + column_threshold: 2, + static_columns: vec![], +}; + const HEADER: &str = r#" @@ -208,10 +213,7 @@ impl<'cfg, Input: Iterator>> Iterator } fn read_input(input: impl std::io::Read) -> Result<(Vec, Config), std::io::Error> { - let mut config = Config { - column_threshold: 2, - static_columns: vec![], - }; + let mut config = DEFAULT_CONFIG; let reader = Reader::new(&mut config, std::io::BufReader::new(input).lines()); reader .collect::, _>>() @@ -722,10 +724,7 @@ mod tests { fn test_render_row() { assert_eq!( render_row( - &Config { - column_threshold: 0, - static_columns: vec![], - }, + &DEFAULT_CONFIG, &["foo".to_owned()], &mut Rowlike::Row(Row { label: "nope".to_owned(), -- 2.50.1 From bc55297895c1343baa24c93330438972a0c4ac89 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 21:38:41 -0700 Subject: [PATCH 067/100] Use Default trait for default Config --- src/lib.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 74a54a2..6d147dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,11 +31,14 @@ 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![], + } + } +} const HEADER: &str = r#" @@ -213,7 +216,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::, _>>() @@ -724,7 +727,7 @@ mod tests { fn test_render_row() { assert_eq!( render_row( - &DEFAULT_CONFIG, + &Config::default(), &["foo".to_owned()], &mut Rowlike::Row(Row { label: "nope".to_owned(), -- 2.50.1 From b4bc28bae91f901f88fdad4621b14da9eb18be06 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 21:41:11 -0700 Subject: [PATCH 068/100] Plumb Config into render_all_leftovers() --- src/lib.rs | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6d147dd..99c454d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -308,7 +308,7 @@ fn render_leftover(notcol: &str, instances: &[Option]) -> HTML { } } -fn render_all_leftovers(row: &Row) -> HTML { +fn render_all_leftovers(config: &Config, row: &Row) -> HTML { let mut order: Vec<_> = row.entries.keys().collect(); order.sort_unstable(); HTML( @@ -338,7 +338,7 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM .iter() .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" )) @@ -695,30 +695,39 @@ 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") ); } -- 2.50.1 From 3135b2cd109024a66320267f3e83592cb75f1d69 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 21:54:51 -0700 Subject: [PATCH 069/100] `!hide` to hide a column --- Changelog | 1 + src/lib.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 37cde58..15fad96 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,7 @@ - Read column threshold from `!col_threshold ` in input - Allow forcing column layout with `!col ` in input - `!colsep` to separate `!col` columns +- `!hide` to hide a column ## [0.3.0] - 2024-10-02 - Center text in each cell diff --git a/src/lib.rs b/src/lib.rs index 99c454d..2111e1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use std::iter::Iterator; struct Config { column_threshold: usize, static_columns: Vec>, + hidden_columns: HashSet, } impl Config { fn apply_command(&mut self, line_num: usize, cmd: &str) -> Result<(), std::io::Error> { @@ -18,6 +19,8 @@ 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" { @@ -36,6 +39,7 @@ impl Default for Config { Self { column_threshold: 2, static_columns: vec![], + hidden_columns: HashSet::new(), } } } @@ -253,7 +257,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() } @@ -309,7 +316,11 @@ fn render_leftover(notcol: &str, instances: &[Option]) -> HTML { } fn render_all_leftovers(config: &Config, row: &Row) -> HTML { - let mut order: Vec<_> = row.entries.keys().collect(); + let mut order: Vec<_> = row + .entries + .keys() + .filter(|&col| !config.hidden_columns.contains(col)) + .collect(); order.sort_unstable(); HTML( order @@ -330,12 +341,14 @@ 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(config, row); @@ -353,6 +366,10 @@ fn render_column_headers(config: &Config, columns: &[String]) -> HTML { String::from(r#""#) + &static_columns .chain(dynamic_columns) + .filter(|ocol| { + ocol.map(|col| !config.hidden_columns.contains(col)) + .unwrap_or(true) + }) .fold(String::new(), |mut acc, ocol| { match ocol { Some(col) => { @@ -730,6 +747,20 @@ mod tests { ), HTML::from("bar: 2, foo") ); + assert_eq!( + render_all_leftovers( + &Config { + column_threshold: 2, + static_columns: vec![], + hidden_columns: HashSet::from(["private".to_owned()]), + }, + &Row { + label: "nope".to_owned(), + entries: HashMap::from([("private".to_owned(), vec![None]),]), + } + ), + HTML::from("") + ); } #[test] @@ -753,6 +784,7 @@ mod tests { &Config { column_threshold: 0, static_columns: vec![Some("foo".to_owned()), Some("bar".to_owned())], + hidden_columns: HashSet::new(), }, &["baz".to_owned()], &mut Rowlike::Row(Row { @@ -774,6 +806,7 @@ mod tests { &Config { column_threshold: 0, static_columns: vec![Some("foo".to_owned()), None, Some("bar".to_owned())], + hidden_columns: HashSet::new(), }, &[], &mut Rowlike::Row(Row { @@ -786,6 +819,42 @@ mod tests { ), HTML::from( r#"nopefr +"# + ) + ); + assert_eq!( + render_row( + &Config { + column_threshold: 0, + static_columns: vec![], + hidden_columns: HashSet::from(["foo".to_owned()]), + }, + &[], + &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()]), + }, + &[], + &mut Rowlike::Row(Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![Some("f".to_owned())]),]), + }) + ), + HTML::from( + r#"nope "# ) ); -- 2.50.1 From e43fba20e88c99acfef26dc7008af115d4e0ac07 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 22:11:07 -0700 Subject: [PATCH 070/100] Release 0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- Changelog | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ccde79..e7073fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.4.0-pre" +version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index d8ab194..5ddedbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.4.0-pre" +version = "0.4.0" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" diff --git a/Changelog b/Changelog index 15fad96..212e410 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## [Unreleased] + +## [0.4.0] - 2024-10-03 - Read column threshold from `!col_threshold ` in input - Allow forcing column layout with `!col ` in input - `!colsep` to separate `!col` columns -- 2.50.1 From bd0b5a3318b3906fb06a837a6bb622d2867bf01b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 3 Oct 2024 22:13:16 -0700 Subject: [PATCH 071/100] Start on 0.5.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7073fc..68592ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.4.0" +version = "0.5.0-pre" diff --git a/Cargo.toml b/Cargo.toml index 5ddedbe..a1e17f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.4.0" +version = "0.5.0-pre" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" -- 2.50.1 From f93f5541f591c6b0f9a9f0091eebd20b363ebb42 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 13:39:00 -0700 Subject: [PATCH 072/100] Factor out column_header_order() --- src/lib.rs | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2111e1e..0873f2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -359,31 +359,34 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM } } -fn render_column_headers(config: &Config, columns: &[String]) -> HTML { +fn column_header_order<'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)) + }) +} + +fn render_column_headers(config: &Config, columns: &[String]) -> HTML { HTML( String::from(r#""#) - + &static_columns - .chain(dynamic_columns) - .filter(|ocol| { - ocol.map(|col| !config.hidden_columns.contains(col)) - .unwrap_or(true) - }) - .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_order(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", ) } -- 2.50.1 From a638dfe78611c069402096f0c702c3f51648962c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 13:48:57 -0700 Subject: [PATCH 073/100] Tests for column_header_order() --- src/lib.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 0873f2e..b4b8bbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -595,6 +595,30 @@ mod tests { ); } + #[test] + fn test_column_header_order() { + let mut cfg = Config::default(); + + assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([Some(&"foo".to_owned())])); + + cfg.static_columns.push(Some("bar".to_owned())); + assert!(column_header_order(&cfg, &["foo".to_owned()]) + .eq([Some(&"bar".to_owned()), Some(&"foo".to_owned())])); + + cfg.static_columns.push(None); + assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([ + Some(&"bar".to_owned()), + None, + Some(&"foo".to_owned()) + ])); + + cfg.hidden_columns.insert("foo".to_owned()); + assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([Some(&"bar".to_owned()), None])); + + cfg.hidden_columns.insert("bar".to_owned()); + assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([None])); + } + #[test] fn test_render_cell() { assert_eq!( -- 2.50.1 From e3feb9dc125d96673d3947c925dd7f1986e16416 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 13:50:14 -0700 Subject: [PATCH 074/100] =?utf8?q?Rename=20column=5Fheader=5Forder=20?= =?utf8?q?=E2=86=92=20column=5Fheader=5Flabels?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b4b8bbc..88c6d8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -359,7 +359,7 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM } } -fn column_header_order<'a>( +fn column_header_labels<'a>( config: &'a Config, columns: &'a [String], ) -> impl Iterator> { @@ -373,7 +373,7 @@ fn column_header_order<'a>( fn render_column_headers(config: &Config, columns: &[String]) -> HTML { HTML( String::from(r#""#) - + &column_header_order(config, columns).fold(String::new(), |mut acc, ocol| { + + &column_header_labels(config, columns).fold(String::new(), |mut acc, ocol| { match ocol { Some(col) => { let col_header = HTML::escape(col); @@ -596,27 +596,27 @@ mod tests { } #[test] - fn test_column_header_order() { + fn test_column_header_labels() { let mut cfg = Config::default(); - assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([Some(&"foo".to_owned())])); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([Some(&"foo".to_owned())])); cfg.static_columns.push(Some("bar".to_owned())); - assert!(column_header_order(&cfg, &["foo".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_order(&cfg, &["foo".to_owned()]).eq([ + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([ Some(&"bar".to_owned()), None, Some(&"foo".to_owned()) ])); cfg.hidden_columns.insert("foo".to_owned()); - assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([Some(&"bar".to_owned()), None])); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([Some(&"bar".to_owned()), None])); cfg.hidden_columns.insert("bar".to_owned()); - assert!(column_header_order(&cfg, &["foo".to_owned()]).eq([None])); + assert!(column_header_labels(&cfg, &["foo".to_owned()]).eq([None])); } #[test] -- 2.50.1 From d669f60d11bad036e4ac6c3da39f067d7bd2153a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 14:28:07 -0700 Subject: [PATCH 075/100] `!label col:new label` to substitute a different label for a column --- Changelog | 1 + src/lib.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 212e410..cab5053 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ ## [Unreleased] +- `!label col:new label` to substitute a different label for a column ## [0.4.0] - 2024-10-03 - Read column threshold from `!col_threshold ` in input diff --git a/src/lib.rs b/src/lib.rs index 88c6d8c..f5d12f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ 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> { @@ -25,6 +26,18 @@ impl Config { 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, @@ -40,6 +53,7 @@ impl Default for Config { column_threshold: 2, static_columns: vec![], hidden_columns: HashSet::new(), + substitute_labels: HashMap::new(), } } } @@ -365,9 +379,15 @@ fn column_header_labels<'a>( ) -> 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)) - }) + 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 { @@ -563,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()); @@ -571,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] @@ -612,6 +642,14 @@ mod tests { 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])); @@ -780,6 +818,7 @@ mod tests { column_threshold: 2, static_columns: vec![], hidden_columns: HashSet::from(["private".to_owned()]), + substitute_labels: HashMap::new(), }, &Row { label: "nope".to_owned(), @@ -812,6 +851,7 @@ mod tests { 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 { @@ -834,6 +874,7 @@ mod tests { 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 { @@ -855,6 +896,7 @@ mod tests { column_threshold: 0, static_columns: vec![], hidden_columns: HashSet::from(["foo".to_owned()]), + substitute_labels: HashMap::new(), }, &[], &mut Rowlike::Row(Row { @@ -873,6 +915,7 @@ mod tests { 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 { -- 2.50.1 From f8c2cab25a0438491b3bfd7cbdf1e5d78f8b40a7 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 18:33:48 -0700 Subject: [PATCH 076/100] Tally marks --- Changelog | 2 ++ src/lib.rs | 78 ++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/Changelog b/Changelog index cab5053..cb3b4c5 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,7 @@ ## [Unreleased] - `!label col:new label` to substitute a different label for a column +- Use Unicode tally marks to coalesce multiple no-colon entries + (If you need a font for these, I recommend "BabelStone Han") ## [0.4.0] - 2024-10-03 - Read column threshold from `!col_threshold ` in input diff --git a/src/lib.rs b/src/lib.rs index f5d12f8..84cb336 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,12 @@ use std::fmt::Write; use std::io::BufRead; use std::iter::Iterator; +fn tally_marks(n: usize) -> String { + let fives = { 0..n / 5 }.map(|_| '𝍸'); + let ones = { 0..n % 5 }.map(|_| '𝍷'); + fives.chain(ones).collect() +} + #[derive(PartialEq, Eq, Debug)] struct Config { column_threshold: usize, @@ -279,29 +285,30 @@ fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { .collect() } -fn render_one_instance(instance: &Option) -> HTML { - match instance { - None => HTML::from("✓"), - Some(instance) => HTML::escape(instance.as_ref()), - } -} - fn render_instances(instances: &[Option]) -> HTML { - let all_empty = instances.iter().all(Option::is_none); - if all_empty && instances.len() == 1 { - HTML::from("") - } else if all_empty { - HTML(format!("{}", instances.len())) - } else { - HTML( - instances - .iter() - .map(render_one_instance) - .map(|html| html.0) // Waiting for slice_concat_trait to stabilize - .collect::>() - .join(" "), - ) + let mut tally = 0; + let mut out = vec![]; + for ins in instances { + match ins { + None => tally += 1, + Some(content) => { + if tally > 0 { + out.push(HTML(tally_marks(tally))); + tally = 0; + } + out.push(HTML::escape(content)); + } + } + } + if tally > 0 { + out.push(HTML(tally_marks(tally))); } + HTML( + out.into_iter() + .map(|html| html.0) // Waiting for slice_concat_trait to stabilize + .collect::>() + .join(" "), + ) } fn render_cell(col: &str, row: &mut Row) -> HTML { @@ -321,10 +328,10 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { fn render_leftover(notcol: &str, instances: &[Option]) -> HTML { let label = HTML::escape(notcol); - let rest = render_instances(instances); - if rest == HTML::from("") { + if instances.len() == 1 && instances[0].is_none() { HTML(format!("{label}")) } else { + let rest = render_instances(instances); HTML(format!("{label}: {rest}")) } } @@ -462,6 +469,21 @@ mod tests { ); } + #[test] + fn test_tally_marks() { + assert_eq!(tally_marks(1), "𝍷"); + assert_eq!(tally_marks(2), "𝍷𝍷"); + assert_eq!(tally_marks(3), "𝍷𝍷𝍷"); + assert_eq!(tally_marks(4), "𝍷𝍷𝍷𝍷"); + assert_eq!(tally_marks(5), "𝍸"); + assert_eq!(tally_marks(6), "𝍸𝍷"); + assert_eq!(tally_marks(7), "𝍸𝍷𝍷"); + assert_eq!(tally_marks(8), "𝍸𝍷𝍷𝍷"); + assert_eq!(tally_marks(9), "𝍸𝍷𝍷𝍷𝍷"); + assert_eq!(tally_marks(10), "𝍸𝍸"); + assert_eq!(tally_marks(11), "𝍸𝍸𝍷"); + } + fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { read_input(input).map(|(rows, _)| rows) } @@ -692,7 +714,7 @@ mod tests { } ), HTML::from( - r#""# + r#"𝍷"# ) ); assert_eq!( @@ -704,7 +726,7 @@ mod tests { } ), HTML::from( - r#"2"# + r#"𝍷𝍷"# ) ); assert_eq!( @@ -731,7 +753,7 @@ mod tests { } ), HTML::from( - r#"5 ✓"# + r#"5 𝍷"# ) ); assert_eq!( @@ -755,7 +777,7 @@ mod tests { } ), HTML::from( - r#""# + r#"𝍷"# ) ); let mut r = Row { @@ -810,7 +832,7 @@ mod tests { ]), } ), - HTML::from("bar: 2, foo") + HTML::from("bar: 𝍷𝍷, foo") ); assert_eq!( render_all_leftovers( -- 2.50.1 From a1a4b3c8f14d52f8b85e7fd72b5c430fe4407483 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 18:47:20 -0700 Subject: [PATCH 077/100] =?utf8?q?Don't=20color=20the=20background=20of=20?= =?utf8?q?cells=20that=20contain=20only=20'=C3=97's?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- Changelog | 1 + src/lib.rs | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index cb3b4c5..5963bc8 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,7 @@ - `!label col:new label` to substitute a different label for a column - Use Unicode tally marks to coalesce multiple no-colon entries (If you need a font for these, I recommend "BabelStone Han") +- Don't color the background of cells that contain only '×'s ## [0.4.0] - 2024-10-03 - Read column threshold from `!col_threshold ` in input diff --git a/src/lib.rs b/src/lib.rs index 84cb336..859b3c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,7 +315,14 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { let row_label = HTML::escape(row.label.as_ref()); let col_label = HTML::escape(col); let instances: Option<&Vec>> = row.entries.get(col); - let class = HTML::from(if instances.is_none() { "" } else { "yes" }); + let is_empty = match instances { + None => true, + Some(is) => is.iter().all(|ins| match ins { + None => false, + Some(content) => content == "×", + }), + }; + let class = HTML::from(if is_empty { "" } else { "yes" }); let contents = match instances { None => HTML::from(""), Some(is) => render_instances(is), @@ -756,6 +763,18 @@ mod tests { r#"5 𝍷"# ) ); + assert_eq!( + render_cell( + "foo", + &mut Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![Some("×".to_owned())])]), + } + ), + HTML::from( + r#"×"# + ) + ); assert_eq!( render_cell( "heart", -- 2.50.1 From b59ad4335c434a858a991137445fd34aaa4c6b21 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 18:49:27 -0700 Subject: [PATCH 078/100] Don't emit useless `class=""` --- src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 859b3c1..d85de02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -322,14 +322,14 @@ fn render_cell(col: &str, row: &mut Row) -> HTML { Some(content) => content == "×", }), }; - let class = HTML::from(if is_empty { "" } else { "yes" }); + let class = HTML::from(if is_empty { "" } else { r#" class="yes""# }); let contents = match instances { None => HTML::from(""), Some(is) => render_instances(is), }; row.entries.remove(col); HTML(format!( - r#"{contents}"# + r#"{contents}"# )) } @@ -697,7 +697,7 @@ mod tests { } ), HTML::from( - r#""# + r#""# ) ); assert_eq!( @@ -709,7 +709,7 @@ mod tests { } ), HTML::from( - r#""# + r#""# ) ); assert_eq!( @@ -772,7 +772,7 @@ mod tests { } ), HTML::from( - r#"×"# + r#"×"# ) ); assert_eq!( @@ -882,7 +882,7 @@ mod tests { }) ), HTML::from( - r#"nopebar + r#"nopebar "# ) ); -- 2.50.1 From 05473926f823daa245534848aee2577cbc0d1b44 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 19:07:52 -0700 Subject: [PATCH 079/100] Usage documentation --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 94752af..fe19e7b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,24 @@ Display a flat text log as an HTML table. +Input format: + + row 1 label + column label for a thing + column label for a different thing: cell contents + + row 2 label + column label for a thing + ... + +Several bang-commands are available to control output formatting: + +* `!col ` to force column ordering, overriding frequency-ordering. +* `!colsep` to leave a insert a gap between columns (skip two lines to leave a gap between rows). +* `!col_threshold ` to set the threshold at which entries get columns rather than being left as notes on the right. Default: 2 +* `!hide ` to omit a column +* `!label :` to change a column's label without needing to change all the input occurrences. + (For the other `tablify` that makes ascii-art tables, see [Text-RecordParser][].) [Text-RecordParser]: https://metacpan.org/dist/Text-RecordParser/view/bin/tablify -- 2.50.1 From 215756150995ba0b10ae8b5d63b17553d251e519 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 19:08:19 -0700 Subject: [PATCH 080/100] Release 0.5.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- Changelog | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68592ce..34e7def 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.5.0-pre" +version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index a1e17f6..35870c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.5.0-pre" +version = "0.5.0" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" diff --git a/Changelog b/Changelog index 5963bc8..d2bc4b4 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## [Unreleased] + +## [0.5.0] - 2024-10-04 - `!label col:new label` to substitute a different label for a column - Use Unicode tally marks to coalesce multiple no-colon entries (If you need a font for these, I recommend "BabelStone Han") -- 2.50.1 From d1d16f97ea7aad7e7006bda3bd39de79fe63ae33 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 19:19:10 -0700 Subject: [PATCH 081/100] Start on 0.5.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34e7def..9109ee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.5.0" +version = "0.5.1-pre" diff --git a/Cargo.toml b/Cargo.toml index 35870c0..20df00e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.5.0" +version = "0.5.1-pre" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" -- 2.50.1 From ba61b04b47e497f8519eed53c78864c08bfc8669 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 19:22:28 -0700 Subject: [PATCH 082/100] Plumb config into render_cell() --- src/lib.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d85de02..70dfb52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -311,7 +311,7 @@ fn render_instances(instances: &[Option]) -> HTML { ) } -fn render_cell(col: &str, row: &mut Row) -> HTML { +fn render_cell(config: &Config, col: &str, row: &mut Row) -> HTML { let row_label = HTML::escape(row.label.as_ref()); let col_label = HTML::escape(col); let instances: Option<&Vec>> = row.entries.get(col); @@ -370,14 +370,14 @@ fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTM .iter() .map(|ocol| match ocol { Some(col) if config.hidden_columns.contains(col) => HTML::from(""), - Some(col) => render_cell(col, row), + Some(col) => render_cell(config, 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)) + .map(|col| render_cell(config, col, row)) .collect::(); let leftovers = render_all_leftovers(config, row); HTML(format!( @@ -690,6 +690,7 @@ mod tests { fn test_render_cell() { assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -702,6 +703,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -714,6 +716,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -726,6 +729,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -738,6 +742,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -753,6 +758,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -765,6 +771,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "nope".to_owned(), @@ -777,6 +784,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "heart", &mut Row { label: "nope".to_owned(), @@ -789,6 +797,7 @@ mod tests { ); assert_eq!( render_cell( + &Config::default(), "foo", &mut Row { label: "bob's".to_owned(), @@ -807,11 +816,11 @@ mod tests { ]), }; assert_eq!(r.entries.len(), 2); - render_cell("foo", &mut r); + render_cell(&Config::default(), "foo", &mut r); assert_eq!(r.entries.len(), 1); - render_cell("bar", &mut r); + render_cell(&Config::default(), "bar", &mut r); assert_eq!(r.entries.len(), 1); - render_cell("baz", &mut r); + render_cell(&Config::default(), "baz", &mut r); assert_eq!(r.entries.len(), 0); } -- 2.50.1 From aa80485a2489892e649ae8815c4710f12b19bd80 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 19:28:29 -0700 Subject: [PATCH 083/100] Fix hover highlight for relabeled columns --- Changelog | 1 + src/lib.rs | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index d2bc4b4..45ae92a 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ ## [Unreleased] +- Fix hover highlight for relabeled columns ## [0.5.0] - 2024-10-04 - `!label col:new label` to substitute a different label for a column diff --git a/src/lib.rs b/src/lib.rs index 70dfb52..c1a4761 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -313,7 +313,12 @@ fn render_instances(instances: &[Option]) -> HTML { fn render_cell(config: &Config, col: &str, row: &mut Row) -> HTML { let row_label = HTML::escape(row.label.as_ref()); - let col_label = HTML::escape(col); + let col_label = HTML::escape( + config + .substitute_labels + .get(col) + .map_or(col, std::string::String::as_str), + ); let instances: Option<&Vec>> = row.entries.get(col); let is_empty = match instances { None => true, @@ -975,6 +980,25 @@ mod tests { ), HTML::from( r#"nope +"# + ) + ); + assert_eq!( + render_row( + &Config { + column_threshold: 0, + static_columns: vec![], + hidden_columns: HashSet::new(), + substitute_labels: HashMap::from([("foo".to_owned(), "bar".to_owned())]), + }, + &["foo".to_owned()], + &mut Rowlike::Row(Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![None])]), + }) + ), + HTML::from( + r#"nope𝍷 "# ) ); -- 2.50.1 From c1c11cfc43014f1516a1f7e4c637622fb10129b8 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 4 Oct 2024 19:29:14 -0700 Subject: [PATCH 084/100] Release 0.5.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- Changelog | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9109ee6..06c0655 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.5.1-pre" +version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 20df00e..5eaaddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.5.1-pre" +version = "0.5.1" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" diff --git a/Changelog b/Changelog index 45ae92a..57c4747 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## [Unreleased] + +## [0.5.1] - 2024-10-04 - Fix hover highlight for relabeled columns ## [0.5.0] - 2024-10-04 -- 2.50.1 From 61a7497779d8e9a7d7b73a53c55d252177d68e1c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 10:23:15 -0700 Subject: [PATCH 085/100] Start on 0.6.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06c0655..cf15f9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.5.1" +version = "0.6.0-pre" diff --git a/Cargo.toml b/Cargo.toml index 5eaaddc..41d676a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.5.1" +version = "0.6.0-pre" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" -- 2.50.1 From defb3aedeec6589c054ebf909d54ef1f94e5d52a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 10:24:06 -0700 Subject: [PATCH 086/100] Lighten background shading --- Changelog | 1 + src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 57c4747..ba419d9 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ ## [Unreleased] +- Lighten background shading ## [0.5.1] - 2024-10-04 - Fix hover highlight for relabeled columns diff --git a/src/lib.rs b/src/lib.rs index c1a4761..2e9c7f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,7 +82,7 @@ const HEADER: &str = r#" tr.key > th > div > div { width: 5em; transform-origin: bottom left; transform: translateX(1em) rotate(-65deg) } td { border: thin solid gray; } td.leftover { text-align: left; border: none; padding-left: .4em; } - td.yes { border: thin solid gray; background-color: #ddd; } + td.yes { border: thin solid gray; background-color: #eee; } /* h/t https://stackoverflow.com/questions/5687035/css-bolding-some-text-without-changing-its-containers-size/46452396#46452396 */ .highlight { text-shadow: -0.06ex 0 black, 0.06ex 0 black; } -- 2.50.1 From 166aa6e27cae04eeddd34e2c395a9371ddc7edee Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 10:41:02 -0700 Subject: [PATCH 087/100] A place to keep per-column mark overrides --- src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2e9c7f6..5077590 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ struct Config { static_columns: Vec>, hidden_columns: HashSet, substitute_labels: HashMap, + mark: HashMap, } impl Config { fn apply_command(&mut self, line_num: usize, cmd: &str) -> Result<(), std::io::Error> { @@ -60,6 +61,7 @@ impl Default for Config { static_columns: vec![], hidden_columns: HashSet::new(), substitute_labels: HashMap::new(), + mark: HashMap::new(), } } } @@ -874,6 +876,7 @@ mod tests { static_columns: vec![], hidden_columns: HashSet::from(["private".to_owned()]), substitute_labels: HashMap::new(), + mark: HashMap::new(), }, &Row { label: "nope".to_owned(), @@ -907,6 +910,7 @@ mod tests { static_columns: vec![Some("foo".to_owned()), Some("bar".to_owned())], hidden_columns: HashSet::new(), substitute_labels: HashMap::new(), + mark: HashMap::new(), }, &["baz".to_owned()], &mut Rowlike::Row(Row { @@ -930,6 +934,7 @@ mod tests { static_columns: vec![Some("foo".to_owned()), None, Some("bar".to_owned())], hidden_columns: HashSet::new(), substitute_labels: HashMap::new(), + mark: HashMap::new(), }, &[], &mut Rowlike::Row(Row { @@ -952,6 +957,7 @@ mod tests { static_columns: vec![], hidden_columns: HashSet::from(["foo".to_owned()]), substitute_labels: HashMap::new(), + mark: HashMap::new(), }, &[], &mut Rowlike::Row(Row { @@ -971,6 +977,7 @@ mod tests { static_columns: vec![Some("foo".to_owned())], hidden_columns: HashSet::from(["foo".to_owned()]), substitute_labels: HashMap::new(), + mark: HashMap::new(), }, &[], &mut Rowlike::Row(Row { @@ -990,6 +997,7 @@ mod tests { static_columns: vec![], hidden_columns: HashSet::new(), substitute_labels: HashMap::from([("foo".to_owned(), "bar".to_owned())]), + mark: HashMap::new(), }, &["foo".to_owned()], &mut Rowlike::Row(Row { -- 2.50.1 From 1ca87bfadfa61703dde1cfc224cec881a68a4f56 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 10:53:42 -0700 Subject: [PATCH 088/100] Plumb config into render_leftover() --- src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5077590..3f3dde5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,7 +340,7 @@ fn render_cell(config: &Config, col: &str, row: &mut Row) -> HTML { )) } -fn render_leftover(notcol: &str, instances: &[Option]) -> HTML { +fn render_leftover(config: &Config, notcol: &str, instances: &[Option]) -> HTML { let label = HTML::escape(notcol); if instances.len() == 1 && instances[0].is_none() { HTML(format!("{label}")) @@ -360,7 +360,13 @@ fn render_all_leftovers(config: &Config, row: &Row) -> HTML { HTML( order .into_iter() - .map(|notcol| render_leftover(notcol, row.entries.get(notcol).expect("Key vanished?!"))) + .map(|notcol| { + render_leftover( + config, + notcol, + row.entries.get(notcol).expect("Key vanished?!"), + ) + }) .map(|html| html.0) // Waiting for slice_concat_trait to stabilize .collect::>() .join(", "), -- 2.50.1 From fdc441dab9c793956ea5c6e4a7c766eb87b16588 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 10:58:40 -0700 Subject: [PATCH 089/100] Pass mark overrides to tally_marks() --- src/lib.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3f3dde5..731457e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ use std::fmt::Write; use std::io::BufRead; use std::iter::Iterator; -fn tally_marks(n: usize) -> String { +fn tally_marks(n: usize, mark: Option<&str>) -> String { let fives = { 0..n / 5 }.map(|_| '𝍸'); let ones = { 0..n % 5 }.map(|_| '𝍷'); fives.chain(ones).collect() @@ -287,7 +287,7 @@ fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { .collect() } -fn render_instances(instances: &[Option]) -> HTML { +fn render_instances(instances: &[Option], mark: Option<&str>) -> HTML { let mut tally = 0; let mut out = vec![]; for ins in instances { @@ -295,7 +295,7 @@ fn render_instances(instances: &[Option]) -> HTML { None => tally += 1, Some(content) => { if tally > 0 { - out.push(HTML(tally_marks(tally))); + out.push(HTML(tally_marks(tally, mark))); tally = 0; } out.push(HTML::escape(content)); @@ -303,7 +303,7 @@ fn render_instances(instances: &[Option]) -> HTML { } } if tally > 0 { - out.push(HTML(tally_marks(tally))); + out.push(HTML(tally_marks(tally, mark))); } HTML( out.into_iter() @@ -332,7 +332,7 @@ fn render_cell(config: &Config, col: &str, row: &mut Row) -> HTML { let class = HTML::from(if is_empty { "" } else { r#" class="yes""# }); let contents = match instances { None => HTML::from(""), - Some(is) => render_instances(is), + Some(is) => render_instances(is, config.mark.get(col).map(String::as_str)), }; row.entries.remove(col); HTML(format!( @@ -345,7 +345,7 @@ fn render_leftover(config: &Config, notcol: &str, instances: &[Option]) if instances.len() == 1 && instances[0].is_none() { HTML(format!("{label}")) } else { - let rest = render_instances(instances); + let rest = render_instances(instances, config.mark.get(notcol).map(String::as_str)); HTML(format!("{label}: {rest}")) } } @@ -491,17 +491,17 @@ mod tests { #[test] fn test_tally_marks() { - assert_eq!(tally_marks(1), "𝍷"); - assert_eq!(tally_marks(2), "𝍷𝍷"); - assert_eq!(tally_marks(3), "𝍷𝍷𝍷"); - assert_eq!(tally_marks(4), "𝍷𝍷𝍷𝍷"); - assert_eq!(tally_marks(5), "𝍸"); - assert_eq!(tally_marks(6), "𝍸𝍷"); - assert_eq!(tally_marks(7), "𝍸𝍷𝍷"); - assert_eq!(tally_marks(8), "𝍸𝍷𝍷𝍷"); - assert_eq!(tally_marks(9), "𝍸𝍷𝍷𝍷𝍷"); - assert_eq!(tally_marks(10), "𝍸𝍸"); - assert_eq!(tally_marks(11), "𝍸𝍸𝍷"); + assert_eq!(tally_marks(1, None), "𝍷"); + assert_eq!(tally_marks(2, None), "𝍷𝍷"); + assert_eq!(tally_marks(3, None), "𝍷𝍷𝍷"); + assert_eq!(tally_marks(4, None), "𝍷𝍷𝍷𝍷"); + assert_eq!(tally_marks(5, None), "𝍸"); + assert_eq!(tally_marks(6, None), "𝍸𝍷"); + assert_eq!(tally_marks(7, None), "𝍸𝍷𝍷"); + assert_eq!(tally_marks(8, None), "𝍸𝍷𝍷𝍷"); + assert_eq!(tally_marks(9, None), "𝍸𝍷𝍷𝍷𝍷"); + assert_eq!(tally_marks(10, None), "𝍸𝍸"); + assert_eq!(tally_marks(11, None), "𝍸𝍸𝍷"); } fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { -- 2.50.1 From eb893cd11452f8101ad16abcb4371285cdff8f1d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 11:04:58 -0700 Subject: [PATCH 090/100] Render tally marks with overridden mark character --- src/lib.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 731457e..4deceff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,14 @@ use std::io::BufRead; use std::iter::Iterator; fn tally_marks(n: usize, mark: Option<&str>) -> String { - let fives = { 0..n / 5 }.map(|_| '𝍸'); - let ones = { 0..n % 5 }.map(|_| '𝍷'); - fives.chain(ones).collect() + match mark { + None => { + let fives = { 0..n / 5 }.map(|_| '𝍸'); + let ones = { 0..n % 5 }.map(|_| '𝍷'); + fives.chain(ones).collect() + } + Some(m) => { 0..n }.map(|_| m).collect(), + } } #[derive(PartialEq, Eq, Debug)] @@ -502,6 +507,11 @@ mod tests { assert_eq!(tally_marks(9, None), "𝍸𝍷𝍷𝍷𝍷"); assert_eq!(tally_marks(10, None), "𝍸𝍸"); assert_eq!(tally_marks(11, None), "𝍸𝍸𝍷"); + assert_eq!(tally_marks(1, Some("x")), "x"); + assert_eq!(tally_marks(4, Some("x")), "xxxx"); + assert_eq!(tally_marks(5, Some("x")), "xxxxx"); + assert_eq!(tally_marks(6, Some("x")), "xxxxxx"); + assert_eq!(tally_marks(2, Some("🍔")), "🍔🍔"); } fn read_rows(input: impl std::io::Read) -> Result, std::io::Error> { @@ -821,6 +831,26 @@ mod tests { r#"𝍷"# ) ); + assert_eq!( + render_cell( + &Config { + column_threshold: 2, + static_columns: vec![], + hidden_columns: HashSet::new(), + substitute_labels: HashMap::new(), + mark: HashMap::from([("foo".to_owned(), "🦄".to_owned())]), + }, + "foo", + &mut Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![None])]), + } + ), + HTML::from( + r#"🦄"# + ) + ); + let mut r = Row { label: "nope".to_owned(), entries: HashMap::from([ @@ -891,6 +921,22 @@ mod tests { ), HTML::from("") ); + assert_eq!( + render_all_leftovers( + &Config { + column_threshold: 2, + static_columns: vec![], + hidden_columns: HashSet::new(), + substitute_labels: HashMap::new(), + mark: HashMap::from([("foo".to_owned(), "🌈".to_owned())]), + }, + &Row { + label: "nope".to_owned(), + entries: HashMap::from([("foo".to_owned(), vec![None, None])]), + } + ), + HTML::from("foo: 🌈🌈") + ); } #[test] -- 2.50.1 From 156f8d37e7451b5f31f02f32b0f0eb07df8c3954 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 11:17:21 -0700 Subject: [PATCH 091/100] `!mark col:X` to override the tally mark character for a column --- Changelog | 1 + README.md | 1 + src/lib.rs | 15 +++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/Changelog b/Changelog index ba419d9..9729182 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,6 @@ ## [Unreleased] - Lighten background shading +- `!mark col:X` to override the tally mark character for a column ## [0.5.1] - 2024-10-04 - Fix hover highlight for relabeled columns diff --git a/README.md b/README.md index fe19e7b..e93375d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Several bang-commands are available to control output formatting: * `!col_threshold ` to set the threshold at which entries get columns rather than being left as notes on the right. Default: 2 * `!hide ` to omit a column * `!label :` to change a column's label without needing to change all the input occurrences. +* `!mark :` to change the character that is used as the tally-mark for that column (For the other `tablify` that makes ascii-art tables, see [Text-RecordParser][].) diff --git a/src/lib.rs b/src/lib.rs index 4deceff..e199470 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,16 @@ impl Config { .substitute_labels .insert(col.to_owned(), label.to_owned()), }; + } else if let Some(directive) = cmd.strip_prefix("mark ") { + match directive.split_once(':') { + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("line {line_num}: Mark missing ':'"), + )) + } + Some((col, label)) => self.mark.insert(col.to_owned(), label.to_owned()), + }; } else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -641,6 +651,7 @@ mod tests { .substitute_labels["foo"], "bar" ); + assert_eq!(read_config(&b"!mark foo:*"[..]).unwrap().mark["foo"], "*"); let bad_command = read_config(&b"!no such command"[..]); assert!(bad_command.is_err()); @@ -653,6 +664,10 @@ mod tests { let bad_sub = read_config(&b"!label foo"[..]); assert!(bad_sub.is_err()); assert!(format!("{bad_sub:?}").contains("line 1: Annotation missing ':'")); + + let bad_mark = read_config(&b"!mark foo"[..]); + assert!(bad_mark.is_err()); + assert!(format!("{bad_mark:?}").contains("line 1: Mark missing ':'")); } #[test] -- 2.50.1 From 5eb5ed212bac4b401588d3a8a02fd0a49c640b9a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 24 Oct 2024 11:30:46 -0700 Subject: [PATCH 092/100] Release 0.6.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- Changelog | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf15f9f..b9b6602 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.6.0-pre" +version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 41d676a..1f452d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.6.0-pre" +version = "0.6.0" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" diff --git a/Changelog b/Changelog index 9729182..d81a456 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## [Unreleased] + +## [0.6.0] - 2024-10-24 - Lighten background shading - `!mark col:X` to override the tally mark character for a column -- 2.50.1 From a9aa3df4e8b7ec0c3af2ed60e9d7620e19a997d4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 02:57:15 -0700 Subject: [PATCH 093/100] Start on 0.6.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9b6602..378f847 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "tablify" -version = "0.6.0" +version = "0.6.1-pre" diff --git a/Cargo.toml b/Cargo.toml index 1f452d4..c94da8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.6.0" +version = "0.6.1-pre" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" -- 2.50.1 From 03b61def1a7e9d1c4dd09d1ffaf5558658afd661 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 03:00:47 -0700 Subject: [PATCH 094/100] Elide elidable lifetime --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e199470..4773b88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,9 +207,7 @@ impl<'cfg, Input: Iterator>> Reader<'cfg, } } } -impl<'cfg, Input: Iterator>> Iterator - for Reader<'cfg, Input> -{ +impl>> Iterator for Reader<'_, Input> { type Item = Result; fn next(&mut self) -> Option { loop { -- 2.50.1 From f9d0e394e4d7731775aa236386b895e2effad788 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 03:02:11 -0700 Subject: [PATCH 095/100] Clippy prefers is_none_or() --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4773b88..fb55610 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -421,7 +421,7 @@ fn column_header_labels<'a>( let dynamic_columns = columns.iter().map(Some); static_columns .chain(dynamic_columns) - .filter(|ocol| ocol.map_or(true, |col| !config.hidden_columns.contains(col))) + .filter(|ocol| ocol.is_none_or(|col| !config.hidden_columns.contains(col))) .map(|ocol| { ocol.map(|col| match config.substitute_labels.get(col) { None => col, -- 2.50.1 From 9321d710d6048b57691ff08a62c93c1c8e30a721 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 03:06:40 -0700 Subject: [PATCH 096/100] Appease clippy::impl_trait_in_params --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index fb55610..ac52b1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -457,7 +457,7 @@ fn render_column_headers(config: &Config, columns: &[String]) -> HTML { /// * there's an i/o error while reading `input` /// * the log has invalid syntax: /// * an indented line with no preceding non-indented line -pub fn tablify(input: impl std::io::Read) -> Result { +pub fn tablify(input: R) -> Result { let (rows, config) = read_input(input)?; let columns = column_order(&config, &rows); Ok(HTML(format!( -- 2.50.1 From cd56923ccd7e883fda4b700bd4ca6b2d2df8f307 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 03:16:51 -0700 Subject: [PATCH 097/100] Appease clippy::pattern_type_mismatch --- src/lib.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ac52b1f..23667a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,7 +224,7 @@ impl>> Iterator for Reader return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose() } InputLine::Blank => return Some(Ok(Rowlike::Spacer)), - InputLine::Entry(col, instance) => match &mut self.row { + InputLine::Entry(col, instance) => match self.row { None => { return Some(Err(std::io::Error::other(format!( "line {}: Entry with no header", @@ -266,8 +266,8 @@ fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { let empty = HashMap::new(); let mut counts: Vec<_> = rows .iter() - .flat_map(|rl| match rl { - Rowlike::Row(r) => r.entries.keys(), + .flat_map(|rl| match *rl { + Rowlike::Row(ref r) => r.entries.keys(), Rowlike::Spacer => empty.keys(), }) .fold(HashMap::new(), |mut cs, col| { @@ -279,7 +279,7 @@ fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> { .into_iter() .map(|(col, n)| (n, col)) .collect(); - counts.sort_unstable_by(|(an, acol), (bn, bcol)| bn.cmp(an).then(acol.cmp(bcol))); + counts.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); counts } fn column_order(config: &Config, rows: &[Rowlike]) -> Vec { @@ -304,9 +304,9 @@ fn render_instances(instances: &[Option], mark: Option<&str>) -> HTML { let mut tally = 0; let mut out = vec![]; for ins in instances { - match ins { + match *ins { None => tally += 1, - Some(content) => { + Some(ref content) => { if tally > 0 { out.push(HTML(tally_marks(tally, mark))); tally = 0; @@ -337,9 +337,9 @@ fn render_cell(config: &Config, col: &str, row: &mut Row) -> HTML { let instances: Option<&Vec>> = row.entries.get(col); let is_empty = match instances { None => true, - Some(is) => is.iter().all(|ins| match ins { + Some(is) => is.iter().all(|ins| match *ins { None => false, - Some(content) => content == "×", + Some(ref content) => content == "×", }), }; let class = HTML::from(if is_empty { "" } else { r#" class="yes""# }); @@ -387,16 +387,16 @@ fn render_all_leftovers(config: &Config, row: &Row) -> HTML { } fn render_row(config: &Config, columns: &[String], rowlike: &mut Rowlike) -> HTML { - match rowlike { + match *rowlike { Rowlike::Spacer => HTML::from("\n"), - Rowlike::Row(row) => { + Rowlike::Row(ref mut row) => { let row_label = HTML::escape(row.label.as_ref()); let static_cells = config .static_columns .iter() - .map(|ocol| match ocol { - Some(col) if config.hidden_columns.contains(col) => HTML::from(""), - Some(col) => render_cell(config, col, row), + .map(|ocol| match *ocol { + Some(ref col) if config.hidden_columns.contains(col) => HTML::from(""), + Some(ref col) => render_cell(config, col, row), None => HTML::from(r#""#), }) .collect::(); -- 2.50.1 From 640a7dd8596b0d1d8fdd1433236540daa7728375 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 03:19:57 -0700 Subject: [PATCH 098/100] Include a git pre-commit hook --- Changelog | 1 + git-pre-commit-hook | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100755 git-pre-commit-hook diff --git a/Changelog b/Changelog index d81a456..f36eb82 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ ## [Unreleased] +- Include a git pre-commit hook ## [0.6.0] - 2024-10-24 - Lighten background shading diff --git a/git-pre-commit-hook b/git-pre-commit-hook new file mode 100755 index 0000000..e40d283 --- /dev/null +++ b/git-pre-commit-hook @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Copy me to .git/hooks/pre-commit + +set -euo pipefail + +if [[ "${GIT_REFLOG_ACTION:-}" == 'rebase (reword)' ]];then + exit 0 +fi + +tmpdir= +cleanup_tmpdir() { + if [[ "$tmpdir" && -e "$tmpdir" ]];then + rm -rf "$tmpdir" + fi +} +trap cleanup_tmpdir EXIT + +# Check out the git index (what's about to be committed) in a temporary +# directory & run the supplied command there. +in_git_index_in_tmpdir() { + tmpdir=$(mktemp -d) + [[ "$tmpdir" && -d "$tmpdir" ]] + start_index=$(sha256sum "${GIT_INDEX_FILE:-.git/index}") + git checkout-index --prefix="$tmpdir/" -a + pushd "$tmpdir" + "$@" + popd + end_index=$(sha256sum "${GIT_INDEX_FILE:-.git/index}") + if [[ "$start_index" != "$end_index" ]];then + echo "Index changed while pre-commit tests were running. Aborting!" + exit 1 + fi +} + +verify() { + cargo test --frozen + cargo clippy --frozen -- -D warnings \ + -W clippy::pedantic \ + -W clippy::clone_on_ref_ptr \ + -W clippy::if_then_some_else_none \ + -W clippy::impl_trait_in_params \ + -W clippy::pattern_type_mismatch \ + -W clippy::shadow_reuse \ + -W clippy::shadow_unrelated \ + -W clippy::str_to_string \ + -W clippy::try_err \ + -A clippy::missing_errors_doc + find . -name target -prune -o -name '*.rs' -exec rustfmt --check -- {} + +} + +in_git_index_in_tmpdir verify -- 2.50.1 From f8a3cb9a959eae63f99cc22f51e509351d983e05 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 16 Jul 2025 03:21:57 -0700 Subject: [PATCH 099/100] Release 0.6.1 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- Changelog | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 378f847..36de8f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "tablify" -version = "0.6.1-pre" +version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index c94da8d..5e72420 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tablify" -version = "0.6.1-pre" +version = "0.6.1" edition = "2021" authors = ["Scott Worley "] description = "Summarize a text log as an HTML table" diff --git a/Changelog b/Changelog index f36eb82..7fd5cc8 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,7 @@ ## [Unreleased] + +## [0.6.1] - 2025-07-16 +- Minor cleanups - Include a git pre-commit hook ## [0.6.0] - 2024-10-24 -- 2.50.1 From 493e21a274f99cfcc29559de89cf5ca1c11cc35c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 17 Nov 2025 10:41:31 -0800 Subject: [PATCH 100/100] slice_concat_trait will never be stabilized. :( From #27747: "We discussed this in the libs team and the consensus is that we will likely never stabilize the Concat and Join traits: these will remain implementation details of the join and concat methods." --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 23667a1..11ee0c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -320,7 +320,7 @@ fn render_instances(instances: &[Option], mark: Option<&str>) -> HTML { } HTML( out.into_iter() - .map(|html| html.0) // Waiting for slice_concat_trait to stabilize + .map(|html| html.0) .collect::>() .join(" "), ) @@ -380,7 +380,7 @@ fn render_all_leftovers(config: &Config, row: &Row) -> HTML { row.entries.get(notcol).expect("Key vanished?!"), ) }) - .map(|html| html.0) // Waiting for slice_concat_trait to stabilize + .map(|html| html.0) .collect::>() .join(", "), ) -- 2.50.1