# 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 :style, 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