| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:io' as io; |
| import 'dart:math'; |
| |
| typedef String CellTextCallback(dynamic item); |
| |
| enum ALIGNMENT { left, center, right } |
| enum TEXTBEHAVIOUR { truncateLeft, truncateRight, wrap } |
| |
| /// [OutputTable] defines a base class for outputting tabular data to outputs. |
| abstract class OutputTable { |
| void addHeader(Column column, CellTextCallback callback); |
| void print(List items); |
| void printToSink(List items, StringSink sink); |
| } |
| |
| /// [ScripTable] outputs items without any formatting except tabs for |
| /// separators. |
| class ScriptTable extends OutputTable { |
| final Map<Column, CellTextCallback> _columns = {}; |
| @override |
| void addHeader(Column column, CellTextCallback callback) { |
| _columns[column] = callback; |
| } |
| |
| @override |
| void print(List items) { |
| printToSink(items, io.stdout); |
| } |
| |
| @override |
| void printToSink(List items, StringSink sink) { |
| if (_columns.length == 0) { |
| return; |
| } |
| // Print headers. |
| var columns = _columns.keys.toList(); |
| columns.forEach((column) { |
| if (column != columns[0]) { |
| sink.write("\t"); |
| } |
| sink.write(column.header); |
| }); |
| sink.writeln(""); |
| |
| // Print items. |
| items.forEach((item) { |
| columns.forEach((column) { |
| if (column != columns[0]) { |
| sink.write("\t"); |
| } |
| sink.write(_columns[column](item)); |
| }); |
| sink.writeln(""); |
| }); |
| } |
| } |
| |
| /// [ConsoleTable] outputs a list of items as a table, width a default width of |
| /// 80 units. Column sizes can be set individually. If zero-width for a column |
| /// is specified, the free available space is distributed equally amongst these. |
| /// The table can be styled by giving it a custom template. |
| class ConsoleTable extends OutputTable { |
| final int width; |
| final Template template; |
| |
| final Map<Column, CellTextCallback> _columns = {}; |
| |
| ConsoleTable({this.width = 80, this.template = minimal}); |
| |
| /// [addHeader] adds a new column to the table. When a row is processed the |
| /// callback given will be invoked with the item for the row. |
| @override |
| void addHeader(Column column, CellTextCallback callback) { |
| _columns[column] = callback; |
| } |
| |
| /// Prints the [items] as a table in columns given by [addHeader] to stdout. |
| @override |
| void print(List items) { |
| printToSink(items, io.stdout); |
| } |
| |
| /// Prints the [items] as a table in columns given by [addHeader] to [sink]. |
| @override |
| void printToSink(List items, StringSink sink) { |
| if (_columns.length == 0) { |
| return; |
| } |
| |
| // Precompute allocated width. |
| var allocatedWidth = 0; |
| var zeroWidthColumns = 0; |
| |
| _columns.keys.forEach((column) { |
| if (column.width == 0) { |
| zeroWidthColumns++; |
| } else { |
| allocatedWidth += column.width; |
| } |
| }); |
| |
| // Free width is the default width, subtracted by the already allocated |
| // width and the column separators. |
| var freeWidth = width - |
| allocatedWidth - |
| (_columns.length - 1) * template.columnDivider.length - |
| 2 * template.leftRightFrame.length; |
| if (zeroWidthColumns > 0 && freeWidth > 0) { |
| // Distribute free width among columns with 0 width; |
| var cellWidth = (freeWidth / zeroWidthColumns).round(); |
| _columns.keys.forEach((column) { |
| if (column.width != 0) { |
| return; |
| } |
| if (zeroWidthColumns == 1) { |
| column._computedWidth = freeWidth; |
| } else { |
| column._computedWidth = cellWidth; |
| freeWidth -= cellWidth; |
| zeroWidthColumns--; |
| } |
| }); |
| } |
| // At this point, all column widths are either calculated or should not be |
| // rendered. |
| _printTopBottomDivider(sink); |
| _printHeader(sink); |
| _printHeaderDivider(sink); |
| bool isFirst = true; |
| items.forEach((item) { |
| if (!isFirst) { |
| _printRowDivider(sink); |
| } |
| var cellStrings = _columns.keys |
| .where((column) => column._computedWidth > 0) |
| .map((column) { |
| var strings = _breakText(_columns[column](item) ?? "", |
| column._computedWidth, column.headerBehaviour); |
| return strings |
| .map((str) => |
| _alignPadText(str, column._computedWidth, column.alignment)) |
| .toList(); |
| }).toList(); |
| _printRowAlignTop(cellStrings, sink); |
| isFirst = false; |
| }); |
| _printTopBottomDivider(sink); |
| } |
| |
| void _printTopBottomDivider(StringSink sink) { |
| if (template.topBottomFrame.length > 0) { |
| sink.writeln("${template.cornerJoin}" |
| "${template.topBottomFrame * (width - 2 * template.cornerJoin.length)}" |
| "${template.cornerJoin}"); |
| } |
| } |
| |
| void _printHeaderDivider(StringSink sink) { |
| if (template.headerDivider.length > 0) { |
| sink.writeln("${template.rowJoin}" |
| "${template.headerDivider * (width - 2 * template.rowJoin.length)}" |
| "${template.rowJoin}"); |
| } |
| } |
| |
| void _printRowDivider(StringSink sink) { |
| if (template.rowDivider.length > 0) { |
| sink.write(template.rowJoin); |
| bool isFirst = true; |
| _columns.keys.forEach((column) { |
| if (column._computedWidth > 0) { |
| if (!isFirst) { |
| sink.write(template.cellJoin); |
| } |
| sink.write(template.rowDivider * column._computedWidth); |
| isFirst = false; |
| } |
| }); |
| sink.writeln(template.rowJoin); |
| } |
| } |
| |
| void _printHeader(StringSink sink) { |
| var cellStrings = _columns.keys |
| .where((column) => column._computedWidth > 0) |
| .map((column) { |
| var strings = _breakText( |
| column.header ?? "", column._computedWidth, column.headerBehaviour); |
| var cellStrings = strings |
| .map((str) => |
| _alignPadText(str, column._computedWidth, column.alignment)) |
| .toList(); |
| return cellStrings; |
| }).toList(); |
| _printRowAlignBase(cellStrings, sink); |
| } |
| |
| void _printRowAlignBase(List<List<String>> strings, StringSink sink) { |
| var height = strings.fold( |
| 0, (prevValue, strings) => max<int>(prevValue, strings.length)); |
| for (var i = 0; i < height; i++) { |
| for (var j = 0; j < _columns.length; j++) { |
| sink.write(j == 0 ? template.leftRightFrame : template.columnDivider); |
| if (height - i - 1 >= strings[j].length) { |
| sink.write(' ' * strings[j][0].length); |
| } else { |
| var index = i - (height - strings[j].length); |
| sink.write(strings[j][index]); |
| } |
| } |
| sink.writeln(template.leftRightFrame); |
| } |
| } |
| |
| void _printRowAlignTop(List<List<String>> strings, StringSink sink) { |
| var height = strings.fold( |
| 0, (prevValue, strings) => max<int>(prevValue, strings.length)); |
| for (var i = 0; i < height; i++) { |
| for (var j = 0; j < _columns.length; j++) { |
| sink.write(j == 0 ? template.leftRightFrame : template.columnDivider); |
| if (i >= strings[j].length) { |
| sink.write(' ' * strings[j][0].length); |
| } else { |
| sink.write(strings[j][i]); |
| } |
| } |
| sink.writeln(template.leftRightFrame); |
| } |
| } |
| |
| List<String> _breakText(String text, int width, TEXTBEHAVIOUR textBehaviour) { |
| if (text.length <= width) { |
| return [text]; |
| } |
| |
| if (textBehaviour == TEXTBEHAVIOUR.truncateLeft || |
| textBehaviour == TEXTBEHAVIOUR.truncateRight) { |
| String truncateString = width <= 4 ? ".." : "..."; |
| int substringLength = (width - truncateString.length); |
| String truncatedString = textBehaviour == TEXTBEHAVIOUR.truncateLeft |
| ? "${truncateString}${text.substring(text.length - substringLength)}" |
| : "${text.substring(0, substringLength)}${truncateString}"; |
| if (truncatedString.length > width) { |
| return [text.substring(0, 1)]; |
| } |
| return [truncatedString]; |
| } |
| |
| // We have to wrap - see if we can find any good places to break. |
| List<String> wrappedStrings = []; |
| int finger = 0; |
| var regexpBlank = new RegExp(r"[ ]"); |
| var regexpIncludeChars = new RegExp(r"[\-_]"); |
| while (finger + width < text.length) { |
| var nicerIndex = text.lastIndexOf(regexpBlank, finger + width); |
| if (nicerIndex > finger) { |
| wrappedStrings.add(text.substring(finger, nicerIndex)); |
| finger = nicerIndex + 1; // change space for return; |
| continue; |
| } |
| nicerIndex = text.lastIndexOf(regexpIncludeChars, finger + width - 1); |
| if (nicerIndex > finger) { |
| wrappedStrings.add(text.substring(finger, nicerIndex + 1)); |
| finger = nicerIndex + 1; |
| continue; |
| } |
| wrappedStrings.add(text.substring(finger, finger + width)); |
| finger = finger + width; |
| } |
| var lastSubString = text.substring(finger); |
| if (!lastSubString.isEmpty) { |
| wrappedStrings.add(lastSubString); |
| } |
| return wrappedStrings; |
| } |
| |
| String _alignPadText(String text, int width, ALIGNMENT alignment) { |
| switch (alignment) { |
| case ALIGNMENT.left: |
| return text.padRight(width); |
| case ALIGNMENT.right: |
| return text.padLeft(width); |
| case ALIGNMENT.center: |
| int half = ((width / 2) + (text.length) / 2).round(); |
| String str = text.padLeft(half); |
| str = str.padRight(width); |
| return str; |
| } |
| return ""; // Stupid type-checker. |
| } |
| } |
| |
| /// [Column] defines a column in a table and should be given a descriptive |
| /// header. |
| class Column { |
| final String header; |
| final int width; |
| final ALIGNMENT alignment; |
| final TEXTBEHAVIOUR headerBehaviour; |
| final TEXTBEHAVIOUR cellBehaviour; |
| int _computedWidth; |
| |
| Column(this.header, |
| {this.width = 0, |
| this.alignment = ALIGNMENT.left, |
| this.headerBehaviour = TEXTBEHAVIOUR.wrap, |
| this.cellBehaviour = TEXTBEHAVIOUR.wrap}) |
| : _computedWidth = width; |
| } |
| |
| /// A minimal template with a single hyphon as row-divider. |
| const Template minimal = const Template(); |
| |
| /// A minimal template with a single hyphon as row-divider. |
| const Template rows = |
| const Template(headerDivider: '=', rowDivider: "-", cellJoin: "-"); |
| |
| /// [Template] holds styling information for a table. Each parameter assumes |
| /// either the empty string or a single character. Let: |
| /// |
| /// headerDivider - 'h' |
| /// columnDivider - 'i' |
| /// rowDivider - 'r' |
| /// topBottomFrame - '=' |
| /// leftRightFrame - 'I' |
| /// rowJoin - 'R' |
| /// cornerJoin 'C' |
| /// cellJoin - 'X' |
| /// |
| /// The below shows an example with the characters used |
| /// |
| /// C===================================================C |
| /// I column 1 i column 2 i column 3 I |
| /// RhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhR |
| /// I row 1 i row 1 i row 1 I |
| /// RrrrrrrrrrrXrrrrrrrrrrXrrrrrrrrrrrrrrrrrrrrrrrrrrrrrR |
| /// I row 2 i row 2 i row 2 I |
| /// RrrrrrrrrrrXrrrrrrrrrrXrrrrrrrrrrrrrrrrrrrrrrrrrrrrrR |
| /// |
| class Template { |
| final String headerDivider; |
| final String columnDivider; |
| final String rowDivider; |
| final String topBottomFrame; |
| final String leftRightFrame; |
| final String rowJoin; |
| final String cornerJoin; |
| final String cellJoin; |
| |
| const Template( |
| {this.headerDivider = "-", |
| this.columnDivider = " ", |
| this.rowDivider = "", |
| this.topBottomFrame = "", |
| this.leftRightFrame = "", |
| this.cornerJoin = "", |
| this.rowJoin = "", |
| this.cellJoin = " "}); |
| } |