From 0cd488748e7797fe5cc7ef1ae1cd41f9641affec Mon Sep 17 00:00:00 2001 From: Valerij Fredriksen Date: Fri, 30 Sep 2022 19:37:05 +0200 Subject: [PATCH] Add Styler --- .../github/freva/asciitable/AsciiTable.java | 45 +++++++++++---- .../freva/asciitable/AsciiTableBuilder.java | 14 ++++- .../com/github/freva/asciitable/Styler.java | 56 +++++++++++++++++++ .../freva/asciitable/AsciiTableTest.java | 54 ++++++++++++++++-- 4 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/github/freva/asciitable/Styler.java diff --git a/src/main/java/com/github/freva/asciitable/AsciiTable.java b/src/main/java/com/github/freva/asciitable/AsciiTable.java index 728f5df..d7be713 100644 --- a/src/main/java/com/github/freva/asciitable/AsciiTable.java +++ b/src/main/java/com/github/freva/asciitable/AsciiTable.java @@ -1,11 +1,14 @@ package com.github.freva.asciitable; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -32,7 +35,7 @@ public class AsciiTable { '╪', '╣', '║', '│', '║', '╟', '─', '┼', '╢', '╠', '═', '╪', '╣', '║', '│', '║', '╚', '═', '╧', '╝'}; - static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] rawColumns, Object[][] data) throws IOException { + static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] rawColumns, Object[][] data, Styler styler) throws IOException { if (border.length != NO_BORDERS.length) throw new IllegalArgumentException("Border characters array must be exactly " + NO_BORDERS.length + " elements long"); @@ -43,10 +46,10 @@ static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] .filter(Column::isVisible) .toArray(Column[]::new); - writeTable(osw, lineSeparator, border, columns, stringData); + writeTable(osw, lineSeparator, border, columns, stringData, styler); } - private static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] columns, String[][] data) throws IOException { + private static void writeTable(OutputStreamWriter osw, String lineSeparator, Character[] border, Column[] columns, String[][] data, Styler styler) throws IOException { int[] colWidths = getColWidths(columns, data); OverflowBehaviour[] overflows = Arrays.stream(columns).map(Column::getOverflowBehaviour).toArray(OverflowBehaviour[]::new); boolean insertNewline = writeLine(osw, colWidths, border[0], border[1], border[2], border[3]); @@ -55,7 +58,8 @@ private static void writeTable(OutputStreamWriter osw, String lineSeparator, Cha HorizontalAlign[] aligns = Arrays.stream(columns).map(Column::getHeaderAlign).toArray(HorizontalAlign[]::new); String[] header = Arrays.stream(columns).map(Column::getHeader).toArray(String[]::new); if (insertNewline) osw.write(lineSeparator); - writeData(osw, colWidths, overflows, aligns, header, border[4], border[5], border[6], lineSeparator); + writeData(osw, colWidths, overflows, aligns, header, border[4], border[5], border[6], lineSeparator, + styler == null ? null : (col, rows) -> styler.styleHeader(columns[col], col, rows)); osw.write(lineSeparator); insertNewline = writeLine(osw, colWidths, border[7], border[8], border[9], border[10]); } @@ -63,7 +67,9 @@ private static void writeTable(OutputStreamWriter osw, String lineSeparator, Cha HorizontalAlign[] dataAligns = Arrays.stream(columns).map(Column::getDataAlign).toArray(HorizontalAlign[]::new); for (int i = 0; i < data.length; i++) { if (insertNewline) osw.write(lineSeparator); - writeData(osw, colWidths, overflows, dataAligns, data[i], border[11], border[12], border[13], lineSeparator); + int row = i; + writeData(osw, colWidths, overflows, dataAligns, data[i], border[11], border[12], border[13], lineSeparator, + styler == null ? null : (col, rows) -> styler.styleCell(columns[col], row, col, rows)); if (i < data.length - 1) { osw.write(lineSeparator); insertNewline = writeLine(osw, colWidths, border[14], border[15], border[16], border[17]); @@ -76,7 +82,8 @@ private static void writeTable(OutputStreamWriter osw, String lineSeparator, Cha String[] footer = Arrays.stream(columns).map(Column::getFooter).toArray(String[]::new); insertNewline = writeLine(osw, colWidths, border[18], border[19], border[20], border[21]); if (insertNewline) osw.write(lineSeparator); - writeData(osw, colWidths, overflows, aligns, footer, border[22], border[23], border[24], lineSeparator); + writeData(osw, colWidths, overflows, aligns, footer, border[22], border[23], border[24], lineSeparator, + styler == null ? null : (col, rows) -> styler.styleFooter(columns[col], col, rows)); } if (border[26] != null) osw.write(lineSeparator); @@ -103,7 +110,8 @@ private static boolean writeLine(OutputStreamWriter osw, int[] colWidths, Charac */ @SuppressWarnings("deprecated") private static void writeData(OutputStreamWriter osw, int[] colWidths, OverflowBehaviour[] overflows, HorizontalAlign[] horizontalAligns, - String[] contents, Character left, Character columnSeparator, Character right, String lineSeparator) throws IOException { + String[] contents, Character left, Character columnSeparator, Character right, String lineSeparator, + BiFunction, List> styler) throws IOException { List> linesContents = IntStream.range(0, colWidths.length) .mapToObj(i -> { String text = i < contents.length && contents[i] != null ? contents[i] : ""; @@ -130,11 +138,19 @@ private static void writeData(OutputStreamWriter osw, int[] colWidths, OverflowB .mapToInt(List::size) .max().orElse(0); + List> justifiedLinesContents = styler == null ? null : IntStream.range(0, colWidths.length) + .mapToObj(col -> styler.apply(col, IntStream.range(0, numLines) + .mapToObj(i -> justify(i < linesContents.get(col).size() ? linesContents.get(col).get(i) : "", horizontalAligns[col], colWidths[col], PADDING)) + .collect(Collectors.toList()))) + .collect(Collectors.toList()); + for (int line = 0; line < numLines; line++) { if (left != null) osw.append(left); for (int col = 0; col < colWidths.length; col++) { - String item = linesContents.get(col).size() <= line ? "" : linesContents.get(col).get(line); - writeJustified(osw, item, horizontalAligns[col], colWidths[col], PADDING); + if (justifiedLinesContents == null) { + String item = linesContents.get(col).size() <= line ? "" : linesContents.get(col).get(line); + writeJustified(osw, item, horizontalAligns[col], colWidths[col], PADDING); + } else osw.write(justifiedLinesContents.get(col).get(line)); if (columnSeparator != null && col != colWidths.length - 1) osw.write(columnSeparator); } if (right != null) osw.append(right); @@ -171,9 +187,14 @@ private static int getNumColumns(Column[] columns, Object[][] data) { .reduce(columns.length, Math::max); } - /** Returns the width of each line in resulting table not counting the line break character(s) */ - private static int getTableWidth(int[] colWidths) { - return Arrays.stream(colWidths).sum() + PADDING * (colWidths.length + 1) - 1; + static String justify(String str, HorizontalAlign align, int length, int minPadding) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(length); + try (OutputStreamWriter osw = new OutputStreamWriter(baos)) { + writeJustified(osw, str, align, length, minPadding); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return baos.toString(); } /** diff --git a/src/main/java/com/github/freva/asciitable/AsciiTableBuilder.java b/src/main/java/com/github/freva/asciitable/AsciiTableBuilder.java index 53b30eb..944c4fe 100644 --- a/src/main/java/com/github/freva/asciitable/AsciiTableBuilder.java +++ b/src/main/java/com/github/freva/asciitable/AsciiTableBuilder.java @@ -7,12 +7,14 @@ import java.io.UncheckedIOException; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.stream.IntStream; public class AsciiTableBuilder { private String lineSeparator = System.lineSeparator(); private Character[] border = AsciiTable.BASIC_ASCII; + private Styler styler; private String[] header; private String[] footer; private Column[] columns; @@ -20,13 +22,19 @@ public class AsciiTableBuilder { /** Set the line separator to use between table rows. Default is {@link System#lineSeparator()}. */ public AsciiTableBuilder lineSeparator(String lineSeparator) { - this.lineSeparator = lineSeparator; + this.lineSeparator = Objects.requireNonNull(lineSeparator, "line separator cannot be null"); return this; } /** Set the table border style. Default is {@link AsciiTable#BASIC_ASCII}. */ public AsciiTableBuilder border(Character[] border) { - this.border = border; + this.border = Objects.requireNonNull(border, "border cannot be null"); + return this; + } + + /** Set the table styler, default is noop */ + public AsciiTableBuilder styler(Styler styler) { + this.styler = Objects.requireNonNull(styler, "styler cannot be null"); return this; } @@ -93,7 +101,7 @@ public void writeTo(OutputStream os) { try { OutputStreamWriter osw = new OutputStreamWriter(os); - AsciiTable.writeTable(osw, lineSeparator, border, columns, data); + AsciiTable.writeTable(osw, lineSeparator, border, columns, data, styler); osw.flush(); } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/src/main/java/com/github/freva/asciitable/Styler.java b/src/main/java/com/github/freva/asciitable/Styler.java new file mode 100644 index 0000000..d726fe4 --- /dev/null +++ b/src/main/java/com/github/freva/asciitable/Styler.java @@ -0,0 +1,56 @@ +package com.github.freva.asciitable; + +import java.util.List; + +/** + * Allows styling the table by adding zero-width characters (e.g. ANSI escape codes) + * to the content. This interface will be invoked just before the data is written, + * the inputs are therefore already split into the lines and the text is justified. + * + * WARNING: If any of the methods add any non-zero-width characters or remove any characters, + * the table will be misaligned. + */ +public interface Styler { + + /** + * Style the cell value, e.g. by adding ANSI escape codes. + * + * @param column Column of the cell + * @param row row number of the cell (0-indexed) + * @param col column number of the cell (0-indexed) + * @param data List of strings (lines) in this cell. Guaranteed to be at least 1 element. Unless the original + * cell data was longer than the max column width AND text overflow behavior is NEWLINE, this will + * contain exactly 1 element. + * @return List of strings (lines) in this cell. + */ + default List styleCell(Column column, int row, int col, List data) { + return data; + } + + /** + * Style the header value, e.g. by adding ANSI escape codes. + * + * @param column Column of the header + * @param col column number of the cell (0-indexed) + * @param data {@link Column#getHeader()}, but split into lines to satisfy {@link Column#getMaxWidth()} and + * {@link Column#getOverflowBehaviour()}, and justified per {@link Column#headerAlign(HorizontalAlign)} + * @return List of strings (lines) in this header cell. + */ + default List styleHeader(Column column, int col, List data) { + return data; + } + + /** + * Style the footer value, e.g. by adding ANSI escape codes. + * + * @param column Column of the footer + * @param col column number of the cell (0-indexed) + * @param data {@link Column#getFooter()} ()}, but split into lines to satisfy {@link Column#getMaxWidth()} and + * {@link Column#getOverflowBehaviour()}, and justified per {@link Column#footerAlign(HorizontalAlign)} + * @return List of strings (lines) in this footer cell. + */ + default List styleFooter(Column column, int col, List data) { + return data; + } + +} diff --git a/src/test/java/com/github/freva/asciitable/AsciiTableTest.java b/src/test/java/com/github/freva/asciitable/AsciiTableTest.java index 3ade44b..749c16a 100644 --- a/src/test/java/com/github/freva/asciitable/AsciiTableTest.java +++ b/src/test/java/com/github/freva/asciitable/AsciiTableTest.java @@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; import java.time.Instant; import java.util.Arrays; import java.util.LinkedHashMap; @@ -12,6 +11,7 @@ import java.util.Locale; import java.util.Map; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import static com.github.freva.asciitable.HorizontalAlign.*; import static org.junit.Assert.assertEquals; @@ -603,6 +603,51 @@ public void calculatesCorrectColumnWidthWithLineBreakInHeaderAndFooter() { assertEquals(expected, actual); } + @Test + public void styler() { + String[] headers = {"First", "Second", "Third"}; + String[][] data = {{"11", "12", "13"}, {"21", "22", "23"}, {"31", "32", "33"}}; + + final String RESET = "\u001B[m"; + final String RED = "\u001B[31m"; + final String GREEN = "\u001B[32m"; + final String YELLOW = "\u001B[33m"; + final String BLUE = "\u001B[34m"; + Styler styler = new Styler() { + @Override + public List styleCell(Column column, int row, int col, List data) { + String color = row % 2 == 0 ? (col % 2 == 0 ? RED : BLUE) : (col % 2 == 0 ? BLUE : RED); + return data.stream().map(line -> color + line + RESET).collect(Collectors.toList()); + } + + @Override + public List styleHeader(Column column, int col, List data) { + String color = col % 2 == 0 ? GREEN : YELLOW; + return data.stream().map(line -> color + line + RESET).collect(Collectors.toList()); + } + + @Override + public List styleFooter(Column column, int col, List data) { + String color = col % 2 == 0 ? YELLOW : GREEN; + return data.stream().map(line -> color + line + RESET).collect(Collectors.toList()); + } + }; + + String actual = AsciiTable.builder().styler(styler).header(headers).footer(headers).data(data).asString(); + assertEquals(String.format( + "+-------+--------+-------+%1$s" + + "|%2$s First %6$s|%3$s Second %6$s|%2$s Third %6$s|%1$s" + + "+-------+--------+-------+%1$s" + + "|%5$s 11 %6$s|%4$s 12 %6$s|%5$s 13 %6$s|%1$s" + + "+-------+--------+-------+\n" + + "|%4$s 21 %6$s|%5$s 22 %6$s|%4$s 23 %6$s|%1$s" + + "+-------+--------+-------+%1$s" + + "|%5$s 31 %6$s|%4$s 32 %6$s|%5$s 33 %6$s|%1$s" + + "+-------+--------+-------+%1$s" + + "|%3$s First %6$s|%2$s Second %6$s|%3$s Third %6$s|%1$s" + + "+-------+--------+-------+", System.lineSeparator(), GREEN, YELLOW, BLUE, RED, RESET), actual); + } + @Test(expected = IllegalArgumentException.class) public void validateTooFewBorderChars() { String[] headers = {"Lorem", "Ipsum", "Dolor", "Sit"}; @@ -641,11 +686,8 @@ private static void assertParagraphs(OverflowBehaviour overflowBehaviour, String } private static void assertJustify(String expected, String str, HorizontalAlign align, int length, int minPadding) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - OutputStreamWriter osw = new OutputStreamWriter(baos); - AsciiTable.writeJustified(osw, str, align, length, minPadding); - osw.flush(); - assertEquals(expected, baos.toString()); + String actual = AsciiTable.justify(str, align, length, minPadding); + assertEquals(expected, actual); } private static class Planet {