This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
#--
|
||||
# Copyright (c) 2008-2009 TJ Holowaychuk <tj@vision-media.ca>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
%w(cell row separator style table table_helper util version).each do |file|
|
||||
require_relative "./terminal-table/#{file}"
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
require 'unicode/display_width'
|
||||
|
||||
module Terminal
|
||||
class Table
|
||||
class Cell
|
||||
##
|
||||
# Cell value.
|
||||
|
||||
attr_reader :value
|
||||
|
||||
##
|
||||
# Column span.
|
||||
|
||||
attr_reader :colspan
|
||||
|
||||
##
|
||||
# Initialize with _options_.
|
||||
|
||||
def initialize options = nil
|
||||
@value, options = options, {} unless Hash === options
|
||||
@value = options.fetch :value, value
|
||||
@alignment = options.fetch :alignment, nil
|
||||
@colspan = options.fetch :colspan, 1
|
||||
@width = options.fetch :width, @value.to_s.size
|
||||
@index = options.fetch :index
|
||||
@table = options.fetch :table
|
||||
end
|
||||
|
||||
def alignment?
|
||||
!@alignment.nil?
|
||||
end
|
||||
|
||||
def alignment
|
||||
@alignment || @table.style.alignment || :left
|
||||
end
|
||||
|
||||
def alignment=(val)
|
||||
supported = %w(left center right)
|
||||
if supported.include?(val.to_s)
|
||||
@alignment = val
|
||||
else
|
||||
raise "Aligment must be one of: #{supported.join(' ')}"
|
||||
end
|
||||
end
|
||||
|
||||
def align(val, position, length)
|
||||
positions = { :left => :ljust, :right => :rjust, :center => :center }
|
||||
val.public_send(positions[position], length)
|
||||
end
|
||||
def lines
|
||||
@value.to_s.split(/\n/)
|
||||
end
|
||||
|
||||
##
|
||||
# Render the cell.
|
||||
|
||||
def render(line = 0)
|
||||
left = " " * @table.style.padding_left
|
||||
right = " " * @table.style.padding_right
|
||||
display_width = Unicode::DisplayWidth.of(Util::ansi_escape(lines[line]))
|
||||
render_width = lines[line].to_s.size - display_width + width
|
||||
align("#{left}#{lines[line]}#{right}", alignment, render_width + @table.cell_padding)
|
||||
end
|
||||
alias :to_s :render
|
||||
|
||||
##
|
||||
# Returns the longest line in the cell and
|
||||
# removes all ANSI escape sequences (e.g. color)
|
||||
|
||||
def value_for_column_width_recalc
|
||||
lines.map{ |s| Util::ansi_escape(s) }.max_by{ |s| Unicode::DisplayWidth.of(s) }
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the width of this cell
|
||||
|
||||
def width
|
||||
padding = (colspan - 1) * @table.cell_spacing
|
||||
inner_width = (1..@colspan).to_a.inject(0) do |w, counter|
|
||||
w + @table.column_width(@index + counter - 1)
|
||||
end
|
||||
inner_width + padding
|
||||
end
|
||||
|
||||
def inspect
|
||||
fields = %i[alignment colspan index value width].map do |name|
|
||||
val = self.instance_variable_get('@'+name.to_s)
|
||||
"@#{name}=#{val.inspect}"
|
||||
end.join(', ')
|
||||
return "#<#{self.class} #{fields}>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
require 'terminal-table' #required as some people require this file directly from their Gemfiles
|
||||
|
||||
include Terminal::Table::TableHelper
|
||||
@@ -0,0 +1,66 @@
|
||||
module Terminal
|
||||
class Table
|
||||
class Row
|
||||
|
||||
##
|
||||
# Row cells
|
||||
|
||||
attr_reader :cells
|
||||
|
||||
attr_reader :table
|
||||
|
||||
##
|
||||
# Initialize with _width_ and _options_.
|
||||
|
||||
def initialize table, array = [], **_kwargs
|
||||
@cell_index = 0
|
||||
@table = table
|
||||
@cells = []
|
||||
array.each { |item| self << item }
|
||||
end
|
||||
|
||||
def add_cell item
|
||||
options = item.is_a?(Hash) ? item : {:value => item}
|
||||
cell = Cell.new(options.merge(:index => @cell_index, :table => @table))
|
||||
@cell_index += cell.colspan
|
||||
@cells << cell
|
||||
end
|
||||
alias << add_cell
|
||||
|
||||
def [] index
|
||||
cells[index]
|
||||
end
|
||||
|
||||
def height
|
||||
cells.map { |c| c.lines.count }.max || 0
|
||||
end
|
||||
|
||||
def render
|
||||
vleft, vcenter, vright = @table.style.vertical
|
||||
(0...height).to_a.map do |line|
|
||||
vleft + cells.map do |cell|
|
||||
cell.render(line)
|
||||
end.join(vcenter) + vright
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def number_of_columns
|
||||
@cells.collect(&:colspan).inject(0, &:+)
|
||||
end
|
||||
|
||||
# used to find indices where we have table '+' crossings.
|
||||
# in cases where the colspan > 1, then we will skip over some numbers
|
||||
# if colspan is always 1, then the list should be incrementing by 1.
|
||||
#
|
||||
# skip 0 entry, because it's the left side.
|
||||
# skip last entry, because it's the right side.
|
||||
# we only care about "+/T" style crossings.
|
||||
def crossings
|
||||
idx = 0
|
||||
@cells[0...-1].map { |c| idx += c.colspan }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
module Terminal
|
||||
class Table
|
||||
class Separator < Row
|
||||
|
||||
##
|
||||
# `prevrow`, `nextrow` contain references to adjacent rows.
|
||||
#
|
||||
# `border_type` is a symbol used to control which type of border is used
|
||||
# on the separator (:top for top-edge, :bot for bottom-edge,
|
||||
# :div for interior, and :strong for emphasized-interior)
|
||||
#
|
||||
# `implicit` is false for user-added separators, and true for
|
||||
# implicit/auto-generated separators.
|
||||
|
||||
def initialize(*args, border_type: :div, implicit: false)
|
||||
super
|
||||
@prevrow, @nextrow = nil, nil
|
||||
@border_type = border_type
|
||||
@implicit = implicit
|
||||
end
|
||||
|
||||
attr_accessor :border_type
|
||||
attr_reader :implicit
|
||||
|
||||
def render
|
||||
left_edge, ctrflat, ctrud, right_edge, ctrdn, ctrup = @table.style.horizontal(border_type)
|
||||
|
||||
prev_crossings = @prevrow.respond_to?(:crossings) ? @prevrow.crossings : []
|
||||
next_crossings = @nextrow.respond_to?(:crossings) ? @nextrow.crossings : []
|
||||
rval = [left_edge]
|
||||
numcols = @table.number_of_columns
|
||||
(0...numcols).each do |idx|
|
||||
rval << ctrflat * (@table.column_width(idx) + @table.cell_padding)
|
||||
pcinc = prev_crossings.include?(idx+1)
|
||||
ncinc = next_crossings.include?(idx+1)
|
||||
border_center = if pcinc && ncinc
|
||||
ctrud
|
||||
elsif pcinc
|
||||
ctrup
|
||||
elsif ncinc
|
||||
ctrdn
|
||||
elsif !ctrud.empty?
|
||||
# special case if the center-up-down intersection is empty
|
||||
# which happens when verticals/intersections are removed. in that case
|
||||
# we do not want to replace with a flat element so return empty-string in else block
|
||||
ctrflat
|
||||
else
|
||||
''
|
||||
end
|
||||
rval << border_center if idx < numcols-1
|
||||
end
|
||||
|
||||
rval << right_edge
|
||||
rval.join
|
||||
end
|
||||
|
||||
# Save off neighboring rows, so that we can use them later in determining
|
||||
# which types of table edges to use.
|
||||
def save_adjacent_rows(prevrow, nextrow)
|
||||
@prevrow = prevrow
|
||||
@nextrow = nextrow
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,284 @@
|
||||
# coding: utf-8
|
||||
require 'forwardable'
|
||||
|
||||
module Terminal
|
||||
class Table
|
||||
|
||||
class Border
|
||||
|
||||
attr_accessor :data, :top, :bottom, :left, :right
|
||||
def initialize
|
||||
@top, @bottom, @left, @right = true, true, true, true
|
||||
end
|
||||
def []=(key, val)
|
||||
@data[key] = val
|
||||
end
|
||||
def [](key)
|
||||
@data[key]
|
||||
end
|
||||
def initialize_dup(other)
|
||||
super
|
||||
@data = other.data.dup
|
||||
end
|
||||
def remove_verticals
|
||||
self.class.const_get("VERTICALS").each { |key| @data[key] = "" }
|
||||
self.class.const_get("INTERSECTIONS").each { |key| @data[key] = "" }
|
||||
end
|
||||
def remove_horizontals
|
||||
self.class.const_get("HORIZONTALS").each { |key| @data[key] = "" }
|
||||
end
|
||||
|
||||
# If @left, return the edge else empty-string.
|
||||
def maybeleft(key) ; @left ? @data[key] : '' ; end
|
||||
|
||||
# If @right, return the edge else empty-string.
|
||||
def mayberight(key) ; @right ? @data[key] : '' ; end
|
||||
|
||||
end
|
||||
|
||||
class AsciiBorder < Border
|
||||
HORIZONTALS = %i[x]
|
||||
VERTICALS = %i[y]
|
||||
INTERSECTIONS = %i[i]
|
||||
|
||||
def initialize
|
||||
super
|
||||
@data = { x: "-", y: "|", i: "+" }
|
||||
end
|
||||
|
||||
# Get vertical border elements
|
||||
# @return [Array] 3-element list of [left, center, right]
|
||||
def vertical
|
||||
[maybeleft(:y), @data[:y], mayberight(:y)] # left, center, right
|
||||
end
|
||||
|
||||
# Get horizontal border elements
|
||||
# @return [Array] a 6 element list of: [i-left, horizontal-bar, i-up/down, i-right, i-down, i-up]
|
||||
def horizontal(_type)
|
||||
x, i = @data[:x], @data[:i]
|
||||
[maybeleft(:i), x, i, mayberight(:i), i, i]
|
||||
end
|
||||
end
|
||||
|
||||
class MarkdownBorder < AsciiBorder
|
||||
def initialize
|
||||
super
|
||||
@top, @bottom = false, false
|
||||
@data = { x: "-", y: "|", i: "|" }
|
||||
end
|
||||
end
|
||||
|
||||
class UnicodeBorder < Border
|
||||
|
||||
ALLOWED_SEPARATOR_BORDER_STYLES = %i[
|
||||
top bot
|
||||
div dash dot3 dot4
|
||||
thick thick_dash thick_dot3 thick_dot4
|
||||
heavy heavy_dash heavy_dot3 heavy_dot4
|
||||
bold bold_dash bold_dot3 bold_dot4
|
||||
double
|
||||
]
|
||||
|
||||
HORIZONTALS = %i[x sx ax bx nx bx_dot3 bx_dot4 bx_dash x_dot3 x_dot4 x_dash]
|
||||
VERTICALS = %i[y yw ye]
|
||||
INTERSECTIONS = %i[nw n ne nd
|
||||
aw ai ae ad au
|
||||
bw bi be bd bu
|
||||
w i e dn up
|
||||
sw s se su]
|
||||
def initialize
|
||||
super
|
||||
@data = {
|
||||
nil => nil,
|
||||
nw: "┌", nx: "─", n: "┬", ne: "┐",
|
||||
yw: "│", y: "│", ye: "│",
|
||||
aw: "╞", ax: "═", ai: "╪", ae: "╡", ad: '╤', au: "╧", # double
|
||||
bw: "┝", bx: "━", bi: "┿", be: "┥", bd: '┯', bu: "┷", # heavy/bold/thick
|
||||
w: "├", x: "─", i: "┼", e: "┤", dn: "┬", up: "┴", # normal div
|
||||
sw: "└", sx: "─", s: "┴", se: "┘",
|
||||
# alternative dots/dashes
|
||||
x_dot4: '┈', x_dot3: '┄', x_dash: '╌',
|
||||
bx_dot4: '┉', bx_dot3: '┅', bx_dash: '╍',
|
||||
}
|
||||
end
|
||||
# Get vertical border elements
|
||||
# @return [Array] 3-element list of [left, center, right]
|
||||
def vertical
|
||||
[maybeleft(:yw), @data[:y], mayberight(:ye)]
|
||||
end
|
||||
|
||||
# Get horizontal border elements
|
||||
# @return [Array] a 6 element list of: [i-left, horizontal-bar, i-up/down, i-right, i-down, i-up]
|
||||
def horizontal(type)
|
||||
raise ArgumentError, "Border type is #{type.inspect}, must be one of #{ALLOWED_SEPARATOR_BORDER_STYLES.inspect}" unless ALLOWED_SEPARATOR_BORDER_STYLES.include?(type)
|
||||
lookup = case type
|
||||
when :top
|
||||
[:nw, :nx, :n, :ne, :n, nil]
|
||||
when :bot
|
||||
[:sw, :sx, :s, :se, nil, :s]
|
||||
when :double
|
||||
# typically used for the separator below the heading row or above a footer row)
|
||||
[:aw, :ax, :ai, :ae, :ad, :au]
|
||||
when :thick, :thick_dash, :thick_dot3, :thick_dot4,
|
||||
:heavy, :heavy_dash, :heavy_dot3, :heavy_dot4,
|
||||
:bold, :bold_dash, :bold_dot3, :bold_dot4
|
||||
# alternate thick/bold border
|
||||
xref = type.to_s.sub(/^(thick|heavy|bold)/,'bx').to_sym
|
||||
[:bw, xref, :bi, :be, :bd, :bu]
|
||||
when :dash, :dot3, :dot4
|
||||
# alternate thin dividers
|
||||
xref = "x_#{type}".to_sym
|
||||
[:w, xref, :i, :e, :dn, :up]
|
||||
else # :div (center, non-emphasized)
|
||||
[:w, :x, :i, :e, :dn, :up]
|
||||
end
|
||||
rval = lookup.map { |key| @data.fetch(key) }
|
||||
rval[0] = '' unless @left
|
||||
rval[3] = '' unless @right
|
||||
rval
|
||||
end
|
||||
end
|
||||
|
||||
# Unicode Border With rounded edges
|
||||
class UnicodeRoundBorder < UnicodeBorder
|
||||
def initialize
|
||||
super
|
||||
@data.merge!({nw: '╭', ne: '╮', sw: '╰', se: '╯'})
|
||||
end
|
||||
end
|
||||
|
||||
# Unicode Border with thick outer edges
|
||||
class UnicodeThickEdgeBorder < UnicodeBorder
|
||||
def initialize
|
||||
super
|
||||
@data = {
|
||||
nil => nil,
|
||||
nw: "┏", nx: "━", n: "┯", ne: "┓", nd: nil,
|
||||
yw: "┃", y: "│", ye: "┃",
|
||||
aw: "┣", ax: "═", ai: "╪", ae: "┫", ad: '╤', au: "╧", # double
|
||||
bw: "┣", bx: "━", bi: "┿", be: "┫", bd: '┯', bu: "┷", # heavy/bold/thick
|
||||
w: "┠", x: "─", i: "┼", e: "┨", dn: "┬", up: "┴", # normal div
|
||||
sw: "┗", sx: "━", s: "┷", se: "┛", su: nil,
|
||||
# alternative dots/dashes
|
||||
x_dot4: '┈', x_dot3: '┄', x_dash: '╌',
|
||||
bx_dot4: '┉', bx_dot3: '┅', bx_dash: '╍',
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# A Style object holds all the formatting information for a Table object
|
||||
#
|
||||
# To create a table with a certain style, use either the constructor
|
||||
# option <tt>:style</tt>, the Table#style object or the Table#style= method
|
||||
#
|
||||
# All these examples have the same effect:
|
||||
#
|
||||
# # by constructor
|
||||
# @table = Table.new(:style => {:padding_left => 2, :width => 40})
|
||||
#
|
||||
# # by object
|
||||
# @table.style.padding_left = 2
|
||||
# @table.style.width = 40
|
||||
#
|
||||
# # by method
|
||||
# @table.style = {:padding_left => 2, :width => 40}
|
||||
#
|
||||
# To set a default style for all tables created afterwards use Style.defaults=
|
||||
#
|
||||
# Terminal::Table::Style.defaults = {:width => 80}
|
||||
#
|
||||
class Style
|
||||
extend Forwardable
|
||||
def_delegators :@border, :vertical, :horizontal, :remove_verticals, :remove_horizontals
|
||||
|
||||
@@defaults = {
|
||||
:border => AsciiBorder.new,
|
||||
:padding_left => 1, :padding_right => 1,
|
||||
:margin_left => '',
|
||||
:width => nil, :alignment => nil,
|
||||
:all_separators => false,
|
||||
}
|
||||
|
||||
## settors/gettor for legacy ascii borders
|
||||
def border_x=(val) ; @border[:x] = val ; end
|
||||
def border_y=(val) ; @border[:y] = val ; end
|
||||
def border_i=(val) ; @border[:i] = val ; end
|
||||
def border_y ; @border[:y] ; end
|
||||
def border_y_width ; Util::ansi_escape(@border[:y]).length ; end
|
||||
|
||||
# Accessor for instance of Border
|
||||
attr_reader :border
|
||||
def border=(val)
|
||||
if val.is_a? Symbol
|
||||
# convert symbol name like :foo_bar to get class FooBarBorder
|
||||
klass_str = val.to_s.split('_').collect(&:capitalize).join + "Border"
|
||||
begin
|
||||
klass = Terminal::Table::const_get(klass_str)
|
||||
@border = klass.new
|
||||
rescue NameError
|
||||
raise "Cannot lookup class Terminal::Table::#{klass_str} from symbol #{val.inspect}"
|
||||
end
|
||||
else
|
||||
@border = val
|
||||
end
|
||||
end
|
||||
|
||||
def border_top=(val) ; @border.top = val ; end
|
||||
def border_bottom=(val) ; @border.bottom = val ; end
|
||||
def border_left=(val) ; @border.left = val ; end
|
||||
def border_right=(val) ; @border.right = val ; end
|
||||
|
||||
def border_top ; @border.top ; end
|
||||
def border_bottom ; @border.bottom ; end
|
||||
def border_left ; @border.left ; end
|
||||
def border_right ; @border.right ; end
|
||||
|
||||
|
||||
attr_accessor :padding_left
|
||||
attr_accessor :padding_right
|
||||
|
||||
attr_accessor :margin_left
|
||||
|
||||
attr_accessor :width
|
||||
attr_accessor :alignment
|
||||
|
||||
attr_accessor :all_separators
|
||||
|
||||
|
||||
def initialize options = {}
|
||||
apply self.class.defaults.merge(options)
|
||||
end
|
||||
|
||||
def apply options
|
||||
options.each do |m, v|
|
||||
__send__ "#{m}=", v
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def defaults
|
||||
klass_defaults = @@defaults.dup
|
||||
# border is an object that needs to be duplicated on instantiation,
|
||||
# otherwise everything will be referencing the same object-id.
|
||||
klass_defaults[:border] = klass_defaults[:border].dup
|
||||
klass_defaults
|
||||
end
|
||||
|
||||
def defaults= options
|
||||
@@defaults = defaults.merge(options)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def on_change attr
|
||||
method_name = :"#{attr}="
|
||||
old_method = method method_name
|
||||
define_singleton_method(method_name) do |value|
|
||||
old_method.call value
|
||||
yield attr.to_sym, value
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,372 @@
|
||||
require 'unicode/display_width'
|
||||
|
||||
module Terminal
|
||||
class Table
|
||||
|
||||
attr_reader :title
|
||||
attr_reader :headings
|
||||
|
||||
##
|
||||
# Generates a ASCII/Unicode table with the given _options_.
|
||||
|
||||
def initialize options = {}, &block
|
||||
@elaborated = false
|
||||
@headings = []
|
||||
@rows = []
|
||||
@column_widths = []
|
||||
self.style = options.fetch :style, {}
|
||||
self.headings = options.fetch :headings, []
|
||||
self.rows = options.fetch :rows, []
|
||||
self.title = options.fetch :title, nil
|
||||
yield_or_eval(&block) if block
|
||||
|
||||
style.on_change(:width) { require_column_widths_recalc }
|
||||
end
|
||||
|
||||
##
|
||||
# Align column _n_ to the given _alignment_ of :center, :left, or :right.
|
||||
|
||||
def align_column n, alignment
|
||||
# nil forces the column method to return the cell itself
|
||||
column(n, nil).each do |cell|
|
||||
cell.alignment = alignment unless cell.alignment?
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Add a row.
|
||||
|
||||
def add_row array
|
||||
row = array == :separator ? Separator.new(self) : Row.new(self, array)
|
||||
@rows << row
|
||||
require_column_widths_recalc unless row.is_a?(Separator)
|
||||
end
|
||||
alias :<< :add_row
|
||||
|
||||
##
|
||||
# Add a separator.
|
||||
|
||||
def add_separator(border_type: :div)
|
||||
@rows << Separator.new(self, border_type: border_type)
|
||||
end
|
||||
|
||||
def cell_spacing
|
||||
cell_padding + style.border_y_width
|
||||
end
|
||||
|
||||
def cell_padding
|
||||
style.padding_left + style.padding_right
|
||||
end
|
||||
|
||||
##
|
||||
# Return column _n_.
|
||||
|
||||
def column n, method = :value, array = rows
|
||||
array.map { |row|
|
||||
# for each cells in a row, find the column with index
|
||||
# just greater than the required one, and go back one.
|
||||
index = col = 0
|
||||
row.cells.each do |cell|
|
||||
break if index > n
|
||||
index += cell.colspan
|
||||
col += 1
|
||||
end
|
||||
cell = row[col - 1]
|
||||
cell && method ? cell.__send__(method) : cell
|
||||
}.compact
|
||||
end
|
||||
|
||||
##
|
||||
# Return _n_ column including headings.
|
||||
|
||||
def column_with_headings n, method = :value
|
||||
column n, method, headings_with_rows
|
||||
end
|
||||
|
||||
##
|
||||
# Return columns.
|
||||
|
||||
def columns
|
||||
(0...number_of_columns).map { |n| column n }
|
||||
end
|
||||
|
||||
##
|
||||
# Return length of column _n_.
|
||||
|
||||
def column_width n
|
||||
column_widths[n] || 0
|
||||
end
|
||||
alias length_of_column column_width # for legacy support
|
||||
|
||||
##
|
||||
# Return total number of columns available.
|
||||
|
||||
def number_of_columns
|
||||
headings_with_rows.map { |r| r.number_of_columns }.max || 0
|
||||
end
|
||||
|
||||
##
|
||||
# Set the headings
|
||||
|
||||
def headings= arrays
|
||||
arrays = [arrays] unless arrays.first.is_a?(Array)
|
||||
@headings = arrays.map do |array|
|
||||
row = Row.new(self, array)
|
||||
require_column_widths_recalc
|
||||
row
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Elaborate rows to form an Array of Rows and Separators with adjacency properties added.
|
||||
#
|
||||
# This is separated from the String rendering so that certain features may be tweaked
|
||||
# before the String is built.
|
||||
|
||||
def elaborate_rows
|
||||
|
||||
buffer = style.border_top ? [Separator.new(self, border_type: :top, implicit: true)] : []
|
||||
unless @title.nil?
|
||||
buffer << Row.new(self, [title_cell_options])
|
||||
buffer << Separator.new(self, implicit: true)
|
||||
end
|
||||
@headings.each do |row|
|
||||
unless row.cells.empty?
|
||||
buffer << row
|
||||
buffer << Separator.new(self, border_type: :double, implicit: true)
|
||||
end
|
||||
end
|
||||
if style.all_separators
|
||||
@rows.each_with_index do |row, idx|
|
||||
# last separator is bottom, others are :div
|
||||
border_type = (idx == @rows.size - 1) ? :bot : :div
|
||||
buffer << row
|
||||
buffer << Separator.new(self, border_type: border_type, implicit: true)
|
||||
end
|
||||
else
|
||||
buffer += @rows
|
||||
buffer << Separator.new(self, border_type: :bot, implicit: true) if style.border_bottom
|
||||
end
|
||||
|
||||
# After all implicit Separators are inserted we need to save off the
|
||||
# adjacent rows so that we can decide what type of intersections to use
|
||||
# based on column spans in the adjacent row(s).
|
||||
buffer.each_with_index do |r, idx|
|
||||
if r.is_a?(Separator)
|
||||
prev_row = idx > 0 ? buffer[idx - 1] : nil
|
||||
next_row = buffer.fetch(idx + 1, nil)
|
||||
r.save_adjacent_rows(prev_row, next_row)
|
||||
end
|
||||
end
|
||||
|
||||
@elaborated = true
|
||||
@rows = buffer
|
||||
end
|
||||
|
||||
##
|
||||
# Render the table.
|
||||
|
||||
def render
|
||||
elaborate_rows unless @elaborated
|
||||
@rows.map { |r| style.margin_left + r.render.rstrip }.join("\n")
|
||||
end
|
||||
alias :to_s :render
|
||||
|
||||
##
|
||||
# Return rows without separator rows.
|
||||
|
||||
def rows
|
||||
@rows.reject { |row| row.is_a? Separator }
|
||||
end
|
||||
|
||||
def rows= array
|
||||
@rows = []
|
||||
array.each { |arr| self << arr }
|
||||
end
|
||||
|
||||
def style=(options)
|
||||
style.apply options
|
||||
end
|
||||
|
||||
def style
|
||||
@style ||= Style.new
|
||||
end
|
||||
|
||||
def title=(title)
|
||||
@title = title
|
||||
require_column_widths_recalc
|
||||
end
|
||||
|
||||
##
|
||||
# Check if _other_ is equal to self. _other_ is considered equal
|
||||
# if it contains the same headings and rows.
|
||||
|
||||
def == other
|
||||
if other.respond_to? :render and other.respond_to? :rows
|
||||
self.headings == other.headings and self.rows == other.rows
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def columns_width
|
||||
column_widths.inject(0) { |s, i| s + i + cell_spacing } + style.border_y_width
|
||||
end
|
||||
|
||||
def recalc_column_widths
|
||||
@require_column_widths_recalc = false
|
||||
n_cols = number_of_columns
|
||||
space_width = cell_spacing
|
||||
return if n_cols == 0
|
||||
|
||||
# prepare rows
|
||||
all_rows = headings_with_rows
|
||||
all_rows << Row.new(self, [title_cell_options]) unless @title.nil?
|
||||
|
||||
# DP states, dp[colspan][index][split_offset] => column_width.
|
||||
dp = []
|
||||
|
||||
# prepare initial value for DP.
|
||||
all_rows.each do |row|
|
||||
index = 0
|
||||
row.cells.each do |cell|
|
||||
cell_value = cell.value_for_column_width_recalc
|
||||
cell_width = Unicode::DisplayWidth.of(cell_value.to_s)
|
||||
colspan = cell.colspan
|
||||
|
||||
# find column width from each single cell.
|
||||
dp[colspan] ||= []
|
||||
dp[colspan][index] ||= [0] # add a fake cell with length 0.
|
||||
dp[colspan][index][colspan] ||= 0 # initialize column length to 0.
|
||||
|
||||
# the last index `colspan` means width of the single column (split
|
||||
# at end of each column), not a width made up of multiple columns.
|
||||
single_column_length = [cell_width, dp[colspan][index][colspan]].max
|
||||
dp[colspan][index][colspan] = single_column_length
|
||||
|
||||
index += colspan
|
||||
end
|
||||
end
|
||||
|
||||
# run DP.
|
||||
(1..n_cols).each do |colspan|
|
||||
dp[colspan] ||= []
|
||||
(0..n_cols-colspan).each do |index|
|
||||
dp[colspan][index] ||= [1]
|
||||
(1...colspan).each do |offset|
|
||||
# processed level became reverse map from width => [offset, ...].
|
||||
left_colspan = offset
|
||||
left_index = index
|
||||
left_width = dp[left_colspan][left_index].keys.first
|
||||
|
||||
right_colspan = colspan - left_colspan
|
||||
right_index = index + offset
|
||||
right_width = dp[right_colspan][right_index].keys.first
|
||||
|
||||
dp[colspan][index][offset] = left_width + right_width + space_width
|
||||
end
|
||||
|
||||
# reverse map it for resolution (max width and short offset first).
|
||||
rmap = {}
|
||||
dp[colspan][index].each_with_index do |width, offset|
|
||||
rmap[width] ||= []
|
||||
rmap[width] << offset
|
||||
end
|
||||
|
||||
# sort reversely and store it back.
|
||||
dp[colspan][index] = Hash[rmap.sort.reverse]
|
||||
end
|
||||
end
|
||||
|
||||
resolve = lambda do |colspan, full_width, index = 0|
|
||||
# stop if reaches the bottom level.
|
||||
return @column_widths[index] = full_width if colspan == 1
|
||||
|
||||
# choose best split offset for partition, or second best result
|
||||
# if first one is not dividable.
|
||||
candidate_offsets = dp[colspan][index].collect(&:last).flatten
|
||||
offset = candidate_offsets[0]
|
||||
offset = candidate_offsets[1] if offset == colspan
|
||||
|
||||
# prepare for next round.
|
||||
left_colspan = offset
|
||||
left_index = index
|
||||
left_width = dp[left_colspan][left_index].keys.first
|
||||
|
||||
right_colspan = colspan - left_colspan
|
||||
right_index = index + offset
|
||||
right_width = dp[right_colspan][right_index].keys.first
|
||||
|
||||
# calculate reference column width, give remaining spaces to left.
|
||||
total_non_space_width = full_width - (colspan - 1) * space_width
|
||||
ref_column_width = total_non_space_width / colspan
|
||||
remainder = total_non_space_width % colspan
|
||||
rem_left_width = [remainder, left_colspan].min
|
||||
rem_right_width = remainder - rem_left_width
|
||||
ref_left_width = ref_column_width * left_colspan +
|
||||
(left_colspan - 1) * space_width + rem_left_width
|
||||
ref_right_width = ref_column_width * right_colspan +
|
||||
(right_colspan - 1) * space_width + rem_right_width
|
||||
|
||||
# at most one width can be greater than the reference width.
|
||||
if left_width <= ref_left_width and right_width <= ref_right_width
|
||||
# use refernce width (evenly partition).
|
||||
left_width = ref_left_width
|
||||
right_width = ref_right_width
|
||||
else
|
||||
# the wider one takes its value, shorter one takes the rest.
|
||||
if left_width > ref_left_width
|
||||
right_width = full_width - left_width - space_width
|
||||
else
|
||||
left_width = full_width - right_width - space_width
|
||||
end
|
||||
end
|
||||
|
||||
# run next round.
|
||||
resolve.call(left_colspan, left_width, left_index)
|
||||
resolve.call(right_colspan, right_width, right_index)
|
||||
end
|
||||
|
||||
full_width = dp[n_cols][0].keys.first
|
||||
unless style.width.nil?
|
||||
new_width = style.width - space_width - style.border_y_width
|
||||
if new_width < full_width
|
||||
raise "Table width exceeds wanted width " +
|
||||
"of #{style.width} characters."
|
||||
end
|
||||
full_width = new_width
|
||||
end
|
||||
|
||||
resolve.call(n_cols, full_width)
|
||||
end
|
||||
|
||||
##
|
||||
# Return headings combined with rows.
|
||||
|
||||
def headings_with_rows
|
||||
@headings + rows
|
||||
end
|
||||
|
||||
def yield_or_eval &block
|
||||
return unless block
|
||||
if block.arity > 0
|
||||
yield self
|
||||
else
|
||||
self.instance_eval(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def title_cell_options
|
||||
{:value => @title, :alignment => :center, :colspan => number_of_columns}
|
||||
end
|
||||
|
||||
def require_column_widths_recalc
|
||||
@require_column_widths_recalc = true
|
||||
end
|
||||
|
||||
def column_widths
|
||||
recalc_column_widths if @require_column_widths_recalc
|
||||
@column_widths
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
module Terminal
|
||||
class Table
|
||||
module TableHelper
|
||||
def table headings = [], *rows, &block
|
||||
Terminal::Table.new :headings => headings.to_a, :rows => rows, &block
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
module Terminal
|
||||
class Table
|
||||
module Util
|
||||
# removes all ANSI escape sequences (e.g. color)
|
||||
def ansi_escape(line)
|
||||
line.to_s.gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '').
|
||||
gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '').
|
||||
gsub(/(\x03|\x1a)/, '')
|
||||
end
|
||||
module_function :ansi_escape
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module Terminal
|
||||
class Table
|
||||
VERSION = '3.0.2'
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user