# 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