├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── lib ├── tty-table.rb └── tty │ └── table │ ├── version.rb │ ├── empty.rb │ ├── renderer │ ├── ascii.rb │ └── unicode.rb │ ├── border │ ├── row_line.rb │ ├── unicode.rb │ ├── ascii.rb │ └── null.rb │ ├── options.rb │ ├── operation │ ├── escape.rb │ ├── filter.rb │ ├── padding.rb │ ├── wrapped.rb │ ├── truncation.rb │ └── alignment.rb │ ├── orientation │ ├── vertical.rb │ └── horizontal.rb │ ├── transformation.rb │ ├── indentation.rb │ ├── error.rb │ ├── orientation.rb │ ├── alignment_set.rb │ ├── operations.rb │ ├── border_options.rb │ ├── validatable.rb │ ├── columns.rb │ ├── renderer.rb │ ├── header.rb │ ├── field.rb │ └── column_constraint.rb ├── .rspec ├── .editorconfig ├── .gitignore ├── Rakefile ├── tasks ├── coverage.rake ├── console.rake └── spec.rake ├── examples ├── basic.rb ├── alignment.rb ├── orientation.rb ├── padding.rb └── resize.rb ├── spec ├── unit │ ├── options │ │ └── access_spec.rb │ ├── row │ │ ├── to_ary_spec.rb │ │ ├── access_spec.rb │ │ ├── data_spec.rb │ │ ├── height_spec.rb │ │ ├── each_spec.rb │ │ ├── call_spec.rb │ │ ├── new_spec.rb │ │ └── equality_spec.rb │ ├── data_spec.rb │ ├── header │ │ ├── to_ary_spec.rb │ │ ├── color_spec.rb │ │ ├── new_spec.rb │ │ ├── set_spec.rb │ │ ├── height_spec.rb │ │ ├── call_spec.rb │ │ └── equality_spec.rb │ ├── alignment_set │ │ ├── to_ary_spec.rb │ │ ├── each_spec.rb │ │ └── new_spec.rb │ ├── header_spec.rb │ ├── columns │ │ ├── total_width_spec.rb │ │ ├── widths_from_spec.rb │ │ └── extract_widths_spec.rb │ ├── operation │ │ ├── escape │ │ │ └── call_spec.rb │ │ ├── filter │ │ │ └── call_spec.rb │ │ ├── truncation │ │ │ └── call_spec.rb │ │ ├── wrapped │ │ │ └── call_spec.rb │ │ └── alignment │ │ │ └── call_spec.rb │ ├── indentation │ │ └── indent_spec.rb │ ├── field │ │ ├── width_spec.rb │ │ ├── lines_spec.rb │ │ ├── new_spec.rb │ │ ├── length_spec.rb │ │ └── equality_spec.rb │ ├── empty_spec.rb │ ├── renderer_spec.rb │ ├── renderer │ │ ├── select_spec.rb │ │ ├── basic │ │ │ ├── new_spec.rb │ │ │ ├── extract_column_widths_spec.rb │ │ │ ├── truncation_spec.rb │ │ │ ├── wrapping_spec.rb │ │ │ ├── indentation_spec.rb │ │ │ ├── options_spec.rb │ │ │ ├── separator_spec.rb │ │ │ ├── filter_spec.rb │ │ │ ├── render_spec.rb │ │ │ ├── coloring_spec.rb │ │ │ ├── multiline_spec.rb │ │ │ ├── padding_spec.rb │ │ │ ├── single_row_separator_spec.rb │ │ │ ├── alignment_spec.rb │ │ │ └── resizing_spec.rb │ │ ├── ascii │ │ │ ├── indentation_spec.rb │ │ │ ├── separator_spec.rb │ │ │ ├── render_spec.rb │ │ │ ├── coloring_spec.rb │ │ │ ├── multiline_spec.rb │ │ │ ├── resizing_spec.rb │ │ │ └── padding_spec.rb │ │ ├── unicode │ │ │ ├── indentation_spec.rb │ │ │ ├── separator_spec.rb │ │ │ ├── render_spec.rb │ │ │ ├── padding_spec.rb │ │ │ └── coloring_spec.rb │ │ ├── render_spec.rb │ │ └── border_spec.rb │ ├── filter_spec.rb │ ├── properties_spec.rb │ ├── border │ │ ├── new_spec.rb │ │ ├── options │ │ │ ├── new_spec.rb │ │ │ └── from_spec.rb │ │ ├── unicode │ │ │ └── rendering_spec.rb │ │ ├── ascii │ │ │ └── rendering_spec.rb │ │ └── null │ │ │ └── rendering_spec.rb │ ├── options_spec.rb │ ├── add_row_spec.rb │ ├── each_spec.rb │ ├── operations │ │ └── new_spec.rb │ ├── utf_spec.rb │ ├── validatable │ │ └── validate_options_spec.rb │ ├── transformation │ │ └── extract_tuples_spec.rb │ ├── validatable_spec.rb │ ├── column_constraint │ │ ├── widths_spec.rb │ │ └── enforce_spec.rb │ ├── render_repeat_spec.rb │ ├── to_s_spec.rb │ ├── each_with_index_spec.rb │ ├── eql_spec.rb │ ├── render_spec.rb │ ├── access_spec.rb │ ├── new_spec.rb │ ├── rotate_spec.rb │ ├── alignment_spec.rb │ ├── render_with_spec.rb │ ├── padding_spec.rb │ └── orientation_spec.rb ├── perf │ └── add_row_spec.rb └── spec_helper.rb ├── Gemfile ├── appveyor.yml ├── .rubocop.yml ├── benchmarks └── speed.rb ├── LICENSE.txt ├── tty-table.gemspec └── CHANGELOG.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: piotrmurach 2 | -------------------------------------------------------------------------------- /lib/tty-table.rb: -------------------------------------------------------------------------------- 1 | require_relative "tty/table" 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --warnings 4 | -------------------------------------------------------------------------------- /lib/tty/table/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | VERSION = "0.12.0" 6 | end # Table 7 | end # TTY 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.rb] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | /bin 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | FileList["tasks/**/*.rake"].each(&method(:import)) 6 | 7 | desc "Run all specs" 8 | task ci: %w[ spec ] 9 | 10 | task default: :spec 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: TTY Community Discussions 4 | url: https://github.com/piotrmurach/tty/discussions 5 | about: Suggest ideas, ask and answer questions 6 | -------------------------------------------------------------------------------- /tasks/coverage.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc "Measure code coverage" 4 | task :coverage do 5 | begin 6 | original, ENV["COVERAGE"] = ENV["COVERAGE"], "true" 7 | Rake::Task["spec"].invoke 8 | ensure 9 | ENV["COVERAGE"] = original 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /tasks/console.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc "Load gem inside irb console" 4 | task :console do 5 | require "irb" 6 | require "irb/completion" 7 | require File.join(__FILE__, "../../lib/tty-table") 8 | ARGV.clear 9 | IRB.start 10 | end 11 | task c: %w[ console ] 12 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../lib/tty-table" 4 | 5 | table = TTY::Table.new(%w[header1 header2], 6 | [%w[a1 a2], %w[b1 b2], %w[c1 c2]]) 7 | puts table.render(:ascii) do |renderer| 8 | renderer.border.separator = :each_row 9 | end 10 | -------------------------------------------------------------------------------- /examples/alignment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../lib/tty-table" 4 | 5 | table = TTY::Table.new header: ["Right align", "Center align", "Left align"] 6 | table << %w[a1 a2 a3] 7 | table << %w[b1 b2 b3] 8 | table << %w[c1 c2 c3] 9 | 10 | puts table.render(:ascii, alignments: %i[right center left]) 11 | -------------------------------------------------------------------------------- /spec/unit/options/access_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # describe TTY::Table::Options, 'options' do 4 | # let(:object) { described_class } 5 | # 6 | # subject { described_class.new({'a' => 1, :b => 2}) } 7 | # 8 | # it '' do 9 | # pending 10 | # expect(subject[:a]).to eql(1) 11 | # end 12 | # end 13 | -------------------------------------------------------------------------------- /spec/unit/row/to_ary_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#to_ary' do 4 | let(:object) { described_class.new data } 5 | let(:data) { ['a', 'b'] } 6 | 7 | subject { object.to_ary } 8 | 9 | it { is_expected.to be_instance_of(Array) } 10 | 11 | it { is_expected.to eq(data) } 12 | end 13 | -------------------------------------------------------------------------------- /spec/unit/data_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '.data' do 4 | it 'gets all table data' do 5 | header = ['h1', 'h2', 'h3'] 6 | rows = [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] 7 | table = described_class.new header, rows 8 | expect(table.data).to eql([header] + rows) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/unit/header/to_ary_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, '#to_ary' do 4 | let(:object) { described_class.new(attributes) } 5 | let(:attributes) { [:id, :name, :age] } 6 | 7 | subject { object.to_ary } 8 | 9 | it { is_expected.to be_instance_of(Array) } 10 | 11 | it { is_expected.to eq(attributes) } 12 | end 13 | -------------------------------------------------------------------------------- /spec/unit/alignment_set/to_ary_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::AlignmentSet, '#to_ary' do 4 | let(:argument) { [:center, :left] } 5 | let(:object) { described_class.new argument } 6 | 7 | subject { object.to_ary } 8 | 9 | it { is_expected.to be_instance_of(Array) } 10 | 11 | it { is_expected.to eq(argument) } 12 | end 13 | -------------------------------------------------------------------------------- /spec/unit/header_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#header' do 4 | let(:header) { [:header1, :header2] } 5 | let(:rows) { [['a1', 'a2'], ['b1', 'b2']] } 6 | let(:object) { described_class } 7 | 8 | subject(:table) { object.new header, rows } 9 | 10 | it { expect(table.header).to be_instance_of(TTY::Table::Header) } 11 | end 12 | -------------------------------------------------------------------------------- /examples/orientation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../lib/tty-table" 4 | 5 | table = TTY::Table.new(header: ["Column 1", "Column 2", "Column 3"]) do |t| 6 | t << ["r1 c1", "r1 c2", "r1 c3"] 7 | t << ["r2 c1", "r2 c2", "r2 c3"] 8 | t << ["r3 c1", "r3 c2", "r3 c3"] 9 | end 10 | 11 | table.orientation = :vertical 12 | 13 | puts table.render(:ascii) 14 | -------------------------------------------------------------------------------- /spec/unit/header/color_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, 'color' do 4 | 5 | context 'when default' do 6 | 7 | end 8 | 9 | context 'when ascii' do 10 | let(:renderer) { :ascii } 11 | let(:red) { "\e[31m" } 12 | let(:clear) { "\e[0m" } 13 | 14 | xit 'renders header background in color' do 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/columns/total_width_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Columns, '#extract_widths!' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | 7 | it 'extract widths' do 8 | table = TTY::Table.new(header, rows) 9 | expect(described_class.total_width(table.data)).to eql(6) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/unit/header/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, '#new' do 4 | it "is empty without arguments" do 5 | header = TTY::Table::Header.new 6 | expect(header).to be_empty 7 | end 8 | 9 | it "isn't empty with attributes" do 10 | header = TTY::Table::Header.new [:id, :name, :age] 11 | expect(header.to_a).to eq([:id, :name, :age]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/unit/operation/escape/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Operation::Escape, '#call' do 4 | let(:object) { described_class } 5 | let(:text) { "太丸\nゴシ\tック体\r" } 6 | let(:field) { TTY::Table::Field.new(text) } 7 | 8 | subject(:operation) { object.new } 9 | 10 | it 'changes field value' do 11 | expect(operation.call(field, 0, 0)).to eql("太丸\\nゴシ\\tック体\\r") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/padding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pastel" 4 | require_relative "../lib/tty-table" 5 | 6 | pastel = Pastel.new 7 | yellow = pastel.yellow.detach 8 | header = [yellow.("Column 1"), yellow.("Column 2"), yellow.("Column 3")] 9 | table = TTY::Table.new(header: header) do |t| 10 | t << %w[11 12 13] 11 | t << %w[21 22 23] 12 | t << %w[31 32 33] 13 | end 14 | 15 | puts table.render(:ascii, padding: [1, 2, 1, 2]) 16 | -------------------------------------------------------------------------------- /examples/resize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pastel' 4 | require_relative "../lib/tty-table" 5 | 6 | pastel = Pastel.new 7 | yellow = pastel.yellow.detach 8 | header = [yellow.("Column 1"), yellow.("Column 2"), yellow.("Column 3")] 9 | table = TTY::Table.new(header: header) do |t| 10 | t << %w[11 12 13] 11 | t << %w[21 22 23] 12 | t << %w[31 32 33] 13 | end 14 | 15 | puts table.render(:ascii, resize: true, padding: [1, 2, 1, 2]) 16 | -------------------------------------------------------------------------------- /spec/unit/header/set_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, '#set' do 4 | let(:object) { described_class } 5 | let(:attributes) { [:id, :name, :age] } 6 | 7 | subject(:header) { object.new } 8 | 9 | it 'sets the value' do 10 | header[0] = :id 11 | expect(header[0]).to eql(:id) 12 | end 13 | 14 | it 'gets the value' do 15 | head = object.new [{value: :id}] 16 | expect(head[0]).to eq(:id) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/operation/filter/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Operation::Filter, '#call' do 4 | let(:object) { described_class } 5 | let(:field) { TTY::Table::Field.new('a1') } 6 | let(:filter) { Proc.new { |val, row, col| 'new' } } 7 | let(:value) { 'new' } 8 | 9 | subject(:operation) { object.new(filter) } 10 | 11 | it 'changes field value' do 12 | expect(operation.call(field, 0, 0)).to eql(value) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/indentation/indent_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Indentation, '.indent' do 4 | context 'when enumerable' do 5 | it 'inserts indentation for each element' do 6 | expect(described_class.indent(['line1'], 2)).to eql([' line1']) 7 | end 8 | end 9 | 10 | context 'when string' do 11 | it 'inserts indentation' do 12 | expect(described_class.indent('line1', 2)).to eql(' line1') 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/alignment_set/each_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::AlignmentSet, '#each' do 4 | let(:alignments) { [:left, :center, :right] } 5 | let(:yields) { [] } 6 | let(:object) { described_class.new alignments } 7 | 8 | subject { object.each { |alignment| yields << alignment } } 9 | 10 | it 'yields each alignment' do 11 | expect { subject }.to change { yields.dup }. 12 | from([]). 13 | to(alignments) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/field/width_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Field, '#width' do 4 | let(:object) { described_class } 5 | 6 | let(:instance) { object.new(value) } 7 | 8 | subject { instance.width } 9 | 10 | context 'with only value' do 11 | let(:value) { 'foo' } 12 | 13 | it { is_expected.to eql(3) } 14 | end 15 | 16 | context 'with hash value' do 17 | let(:value) { "foo\nbaar" } 18 | 19 | it { is_expected.to eql(7) } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest new functionality 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the problem 10 | 11 | A brief description of the problem you're trying to solve. 12 | 13 | ### How would the new feature work? 14 | 15 | A short explanation of the new feature. 16 | 17 | ``` 18 | Example code that shows possible usage 19 | ``` 20 | 21 | ### Drawbacks 22 | 23 | Can you see any potential drawbacks? 24 | -------------------------------------------------------------------------------- /spec/unit/field/lines_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Field, '#lines' do 4 | let(:object) { described_class.new value } 5 | 6 | subject { object.lines } 7 | 8 | context 'with escaped value' do 9 | let(:value) { "Multi\nLine" } 10 | 11 | it { is_expected.to eql(["Multi", "Line"]) } 12 | end 13 | 14 | context 'with unescaped value' do 15 | let(:value) { "Multi\\nLine" } 16 | 17 | it { is_expected.to eql(["Multi\\nLine"]) } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/perf/add_row_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec-benchmark" 4 | 5 | RSpec.describe TTY::Table, "#<<" do 6 | include RSpec::Benchmark::Matchers 7 | 8 | let(:array) { [] } 9 | let(:table) { described_class.new } 10 | let(:row) { %w[YYYY-MM-DD Something 123.456] } 11 | 12 | it "performs slower than adding elements to an array" do 13 | expect { 14 | table << row 15 | }.to perform_slower_than { 16 | array << row 17 | }.at_most(30).times 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/empty_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#empty?' do 4 | let(:header) { ['Header1', 'Header2'] } 5 | let(:object) { described_class.new header, rows } 6 | 7 | subject { object.empty? } 8 | 9 | context 'with rows containing no entries' do 10 | let(:rows) { [] } 11 | 12 | it { is_expected.to eq(true) } 13 | end 14 | 15 | context 'with rows containing an entry' do 16 | let(:rows) { [['a1']] } 17 | 18 | it { is_expected.to eq(false) } 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/tty/table/empty.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | class Empty < TTY::Table 6 | 7 | ZERO_ROW = [].freeze 8 | 9 | def self.new(header, rows = ZERO_ROW) 10 | super.new(header, rows) 11 | end 12 | 13 | def each 14 | return to_enum unless block_given? 15 | self 16 | end 17 | 18 | def size 19 | 0 20 | end 21 | 22 | def width 23 | 0 24 | end 25 | 26 | end # Empty 27 | end # Table 28 | end # TTY 29 | -------------------------------------------------------------------------------- /lib/tty/table/renderer/ascii.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../border/ascii" 4 | require_relative "../renderer/basic" 5 | 6 | module TTY 7 | class Table 8 | module Renderer 9 | class ASCII < Basic 10 | # Create ASCII renderer 11 | # 12 | # @api private 13 | def initialize(table, options = {}) 14 | super(table, options.merge(border_class: TTY::Table::Border::ASCII)) 15 | end 16 | end # ASCII 17 | end # Renderer 18 | end # Table 19 | end # TTY 20 | -------------------------------------------------------------------------------- /spec/unit/renderer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#renderer' do 4 | let(:object) { described_class } 5 | let(:header) { ['h1', 'h2'] } 6 | let(:rows) { [['a1', 'a2'], ['b1', 'b2']] } 7 | 8 | subject(:table) { object.new(header, rows).renderer } 9 | 10 | it 'creates new renderer' do 11 | expect(subject).to be_kind_of(TTY::Table::Renderer::Basic) 12 | end 13 | 14 | it 'chains calls on renderer' do 15 | expect(subject.render).to eql("h1 h2\na1 a2\nb1 b2") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/row/access_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#access' do 4 | let(:object) { described_class.new [] } 5 | 6 | before { object[attribute] = value} 7 | 8 | subject { object[attribute] } 9 | 10 | context 'when integer' do 11 | let(:attribute) { 0 } 12 | let(:value) { 1 } 13 | 14 | it { is_expected.to eq(1) } 15 | end 16 | 17 | context 'when symbol' do 18 | let(:attribute) { :id } 19 | let(:value) { 1 } 20 | 21 | it { is_expected.to eq(1) } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/unit/renderer/select_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer, '#select' do 4 | let(:klass) { ::Class.new } 5 | let(:instance) { described_class } 6 | 7 | subject { instance.select(renderer) } 8 | 9 | context 'with basic' do 10 | let(:renderer) { :basic } 11 | 12 | it { is_expected.to be(TTY::Table::Renderer::Basic) } 13 | end 14 | 15 | context 'with unicode' do 16 | let(:renderer) { :unicode } 17 | 18 | it { is_expected.to be(TTY::Table::Renderer::Unicode) } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe the change 2 | What does this Pull Request do? 3 | 4 | ### Why are we doing this? 5 | Any related context as to why is this is a desirable change. 6 | 7 | ### Benefits 8 | How will the library improve? 9 | 10 | ### Drawbacks 11 | Possible drawbacks applying this change. 12 | 13 | ### Requirements 14 | 15 | - [ ] Tests written & passing locally? 16 | - [ ] Code style checked? 17 | - [ ] Rebased with `master` branch? 18 | - [ ] Documentation updated? 19 | - [ ] Changelog updated? 20 | -------------------------------------------------------------------------------- /spec/unit/filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#filter' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | 7 | subject(:table) { described_class.new(header, rows) } 8 | 9 | it 'filters fields' do 10 | expect(table.render do |renderer| 11 | renderer.filter = proc do |val, row, col| 12 | (col == 1 && row > 0) ? val.capitalize : val 13 | end 14 | end).to eq unindent(<<-EOS) 15 | h1 h2 h3 16 | a1 A2 a3 17 | b1 B2 b3 18 | EOS 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/row/data_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#data' do 4 | let(:object) { described_class.new data, header } 5 | let(:data) { ['a'] } 6 | 7 | subject { object.to_hash } 8 | 9 | context 'without attributes' do 10 | let(:header) { nil } 11 | 12 | it { is_expected.to be_instance_of(Hash) } 13 | 14 | it { is_expected.to eql(0 => 'a') } 15 | end 16 | 17 | context 'with attributes' do 18 | let(:header) { [:id] } 19 | 20 | it { is_expected.to be_instance_of(Hash) } 21 | 22 | it { is_expected.to eql(id: 'a') } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/unit/properties_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'properties' do 4 | let(:rows) {[['a1', 'a2', 'a3'], ['b1', 'b2', 'c3']] } 5 | 6 | subject(:table) { described_class.new rows } 7 | 8 | it { expect(table.width).to eq(6) } 9 | 10 | it { expect(table.rows_size).to eq(2) } 11 | 12 | it { expect(table.columns_size).to eq(3) } 13 | 14 | it { expect(table.size).to eq([2,3]) } 15 | 16 | context 'no size' do 17 | let(:rows) { [] } 18 | 19 | it { expect(table.rows_size).to eq(0) } 20 | 21 | it { expect(table.columns_size).to eq(0) } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/unit/row/height_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#height' do 4 | let(:object) { described_class.new row } 5 | 6 | subject { object.height } 7 | 8 | context 'single row' do 9 | let(:row) { ['a1', 'b1'] } 10 | 11 | it { expect(subject).to eql(1) } 12 | end 13 | 14 | context 'non escaped multiline' do 15 | let(:row) { ["a1\na2\na3", 'b1'] } 16 | 17 | it { expect(subject).to eql(3)} 18 | end 19 | 20 | context 'escaped multiline' do 21 | let(:row) { ["a1\\na2\\na3", 'b1'] } 22 | 23 | it { expect(subject).to eql(1) } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0") 6 | gem "rspec-benchmark" 7 | end 8 | if RUBY_VERSION == "2.0.0" 9 | gem "json", "2.4.1" 10 | gem "rake", "12.3.3" 11 | end 12 | 13 | group :tools do 14 | gem "yard", "~> 0.9.12" 15 | end 16 | 17 | group :metrics do 18 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5.0") 19 | gem "coveralls_reborn", "~> 0.21.0" 20 | gem "simplecov", "~> 0.21.0" 21 | end 22 | gem "yardstick", "~> 0.9.9" 23 | end 24 | 25 | group :benchmarks do 26 | gem "benchmark-ips", "~> 2.7.2" 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/header/height_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, '#height' do 4 | let(:object) { described_class.new header } 5 | 6 | subject { object.height } 7 | 8 | context 'single row' do 9 | let(:header) { ['h1', 'h1'] } 10 | 11 | it { expect(subject).to eql(1) } 12 | end 13 | 14 | context 'non escaped multiline' do 15 | let(:header) { ["h1\nh1\nh1", 'h2'] } 16 | 17 | it { expect(subject).to eql(3)} 18 | end 19 | 20 | context 'escaped multiline' do 21 | let(:header) { ["h1\\h1\\h1", 'h2'] } 22 | 23 | it { expect(subject).to eql(1) } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, '.new' do 4 | let(:header) { ['h1'] } 5 | let(:rows) { [['a1']] } 6 | 7 | subject(:renderer) { described_class } 8 | 9 | context 'without table' do 10 | let(:table) { nil } 11 | 12 | it { 13 | expect { 14 | renderer.new(table) 15 | }.to raise_error(TTY::Table::ArgumentRequired) 16 | } 17 | end 18 | 19 | context 'with table' do 20 | let(:table) { TTY::Table.new(header, rows) } 21 | 22 | it { expect { renderer.new(table) }.not_to raise_error } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/tty/table/border/row_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | class Border 6 | # A class for a table row line chars manipulation 7 | class RowLine < Struct.new(:left, :center, :right) 8 | # Colorize characters with a given style 9 | # 10 | # @api public 11 | def colorize(border, style) 12 | self.right = border.set_color(style, right) 13 | self.center = border.set_color(style, center) 14 | self.left = border.set_color(style, left) 15 | end 16 | end # RowLine 17 | end # Border 18 | end # Table 19 | end # TTY 20 | -------------------------------------------------------------------------------- /lib/tty/table/renderer/unicode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../border/unicode" 4 | 5 | module TTY 6 | class Table 7 | module Renderer 8 | # Unicode representation of table renderer 9 | # 10 | # @api private 11 | class Unicode < Basic 12 | # Create Unicode renderer 13 | # 14 | # @param [Table] table 15 | # 16 | # @api private 17 | def initialize(table, options = {}) 18 | super(table, options.merge(border_class: TTY::Table::Border::Unicode)) 19 | end 20 | end # Unicode 21 | end # Renderer 22 | end # Table 23 | end # TTY 24 | -------------------------------------------------------------------------------- /spec/unit/border/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Border, '#new' do 4 | let(:row) { [] } 5 | 6 | subject(:instance) { klass.new row, [0,0,0,0] } 7 | 8 | context 'when abstract' do 9 | let(:klass) { described_class } 10 | 11 | it { expect { instance }.to raise_error(NotImplementedError) } 12 | end 13 | 14 | context 'when concrete' do 15 | let(:klass) { 16 | Class.new do 17 | def initialize(row, padding); end 18 | end 19 | } 20 | 21 | it { expect { instance }.to_not raise_error() } 22 | 23 | it { is_expected.to be_instance_of klass } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'options' do 4 | let(:rows) { [['a1', 'a2'], ['b1', 'b2']] } 5 | let(:widths) { nil } 6 | let(:aligns) { [] } 7 | let(:object) { described_class } 8 | let(:options) { 9 | { 10 | column_widths: widths, 11 | column_aligns: aligns, 12 | renderer: :basic 13 | } 14 | } 15 | 16 | subject(:table) { object.new rows, options } 17 | 18 | it { expect(table.header).to be_nil } 19 | 20 | it { expect(table.rows).to eq(rows) } 21 | 22 | it { expect(table.orientation).to be_kind_of TTY::Table::Orientation::Horizontal } 23 | end 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report something not working correctly or as expected 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the problem 10 | 11 | A brief description of the issue. 12 | 13 | ### Steps to reproduce the problem 14 | 15 | ``` 16 | Your code here to reproduce the issue 17 | ``` 18 | 19 | ### Actual behaviour 20 | 21 | What happened? This could be a description, log output, error raised etc. 22 | 23 | ### Expected behaviour 24 | 25 | What did you expect to happen? 26 | 27 | ### Describe your environment 28 | 29 | * OS version: 30 | * Ruby version: 31 | * TTY::Table version: 32 | -------------------------------------------------------------------------------- /lib/tty/table/options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | module TTY 6 | class Table 7 | # Structure for holding table options with indifferent access 8 | class Options < DelegateClass(Hash) 9 | 10 | def initialize(hash={}, &block) 11 | super(&block) 12 | 13 | hash.each do |key, value| 14 | self[convert_key(key)] = valu 15 | end 16 | end 17 | 18 | def []=(key, value) 19 | super(convert_key(key), value) 20 | end 21 | 22 | def convert_key(key) 23 | key.is_a?(Symbol) ? key.to_s : key 24 | end 25 | 26 | end # Options 27 | end # Table 28 | end # TTY 29 | -------------------------------------------------------------------------------- /spec/unit/add_row_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#<<' do 4 | let(:rows) { ['a', 'b', 'c'] } 5 | let(:object) { described_class } 6 | 7 | subject(:table) { object[rows] } 8 | 9 | context 'with primitive values' do 10 | let(:row) { [1, 2, 3] } 11 | 12 | it 'extracts values correctly' do 13 | table << row 14 | expect(table.to_a.last).to eql(row) 15 | end 16 | end 17 | 18 | context 'with complex values' do 19 | let(:row) { [1, { value: 2 }, 3] } 20 | 21 | it 'extracts values correctly' do 22 | table << row 23 | expect(table.to_a.last).to eql([1,2,3]) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/unit/each_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#each' do 4 | let(:header) { ['Header1'] } 5 | let(:rows) { [['a1'], ['b1']] } 6 | 7 | subject(:table) { described_class.new(header, rows) } 8 | 9 | context 'with no block' do 10 | it { expect(table.each).to be_instance_of(to_enum.class) } 11 | 12 | it 'yields the expected values' do 13 | expect(table.each.to_a).to eql(table.to_a) 14 | end 15 | end 16 | 17 | context 'with block' do 18 | it 'yields each row' do 19 | yields = [] 20 | table.each { |row| yields << row } 21 | expect(yields).to eql(table.to_a) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/unit/alignment_set/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::AlignmentSet, '#new' do 4 | let(:object) { described_class } 5 | 6 | subject(:alignment_set) { object.new(argument) } 7 | 8 | context 'with no argument' do 9 | let(:argument) { [] } 10 | 11 | it { is_expected.to be_kind_of(Enumerable) } 12 | 13 | it { is_expected.to be_instance_of(object) } 14 | 15 | it { expect(alignment_set.to_a).to eq([]) } 16 | end 17 | 18 | context 'with argument' do 19 | let(:argument) { [:center, :left] } 20 | 21 | it { is_expected.to be_instance_of(object) } 22 | 23 | it { expect(alignment_set.to_a).to eq(argument) } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/field/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Field, '#new' do 4 | let(:object) { described_class } 5 | 6 | subject { object.new value } 7 | 8 | context 'with only value' do 9 | let(:value) { 'foo' } 10 | 11 | it { is_expected.to be_instance_of(object) } 12 | 13 | it { expect(subject.value).to eql(value) } 14 | 15 | it { expect(subject.height).to eql(1) } 16 | end 17 | 18 | context 'with hash value' do 19 | let(:value) { { :value => 'foo' } } 20 | 21 | it { is_expected.to be_instance_of(object) } 22 | 23 | it { expect(subject.value).to eql('foo') } 24 | 25 | it { expect(subject.height).to eql(1) } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/operations/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Operations, '#new' do 4 | let(:object) { described_class } 5 | let(:row) { [1,2,3] } 6 | let(:table) { TTY::Table.new :rows => [row] } 7 | let(:callable) { 8 | Class.new do 9 | def call(val, row, col) 10 | val.value= val.value + 1 11 | end 12 | end 13 | } 14 | let(:instance) { callable.new } 15 | 16 | subject { object.new } 17 | 18 | before { subject.add(:alignment, instance) } 19 | 20 | it 'stores away operations' do 21 | expect(subject[:alignment]).to include(instance) 22 | end 23 | 24 | it 'applies selected operations' do 25 | subject.apply_to(table, :alignment) 26 | expect(table.data[0]).to eql([2,3,4]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/header/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, '#call' do 4 | let(:object) { described_class.new(attributes) } 5 | let(:attributes) { [:id, :name, :age] } 6 | 7 | subject { object[attribute] } 8 | 9 | context 'with a known attribute' do 10 | context 'when symbol' do 11 | let(:attribute) { :age } 12 | 13 | it { is_expected.to eq(2) } 14 | end 15 | 16 | context 'when integer' do 17 | let(:attribute) { 1 } 18 | 19 | it { is_expected.to eq(:name) } 20 | end 21 | end 22 | 23 | context 'with an unknown attribute' do 24 | let(:attribute) { :mine } 25 | 26 | it { expect { subject }.to raise_error(TTY::Table::UnknownAttributeError, "the header 'mine' is unknown")} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | skip_commits: 3 | files: 4 | - "benchmarks/**" 5 | - "examples/**" 6 | - "*.md" 7 | install: 8 | - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% 9 | - gem install bundler -v '< 2.0' 10 | - bundle install 11 | before_test: 12 | - ruby -v 13 | - gem -v 14 | - bundle -v 15 | build: off 16 | test_script: 17 | - bundle exec rake ci 18 | environment: 19 | matrix: 20 | - ruby_version: "200" 21 | - ruby_version: "200-x64" 22 | - ruby_version: "21" 23 | - ruby_version: "21-x64" 24 | - ruby_version: "22" 25 | - ruby_version: "22-x64" 26 | - ruby_version: "23" 27 | - ruby_version: "23-x64" 28 | - ruby_version: "24" 29 | - ruby_version: "24-x64" 30 | - ruby_version: "25" 31 | - ruby_version: "25-x64" 32 | - ruby_version: "26" 33 | - ruby_version: "26-x64" 34 | -------------------------------------------------------------------------------- /lib/tty/table/operation/escape.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | module Operation 6 | # A class responsible for escaping special chars in a table field 7 | # 8 | # @api private 9 | class Escape 10 | # Escape special characters in a table field 11 | # 12 | # @param [TTY::Table::Field] field 13 | # 14 | # @param [Integer] row 15 | # the field row index 16 | # 17 | # @param [Integer] col 18 | # the field column index 19 | # 20 | # @api public 21 | def call(field, row, col) 22 | field.content.gsub(/(\t|\r|\n)/) do |val| 23 | val.dump.gsub('"', "") 24 | end 25 | end 26 | end # Escape 27 | end # Operation 28 | end # Table 29 | end # TTY 30 | -------------------------------------------------------------------------------- /spec/unit/operation/truncation/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Operation::Truncation, '#call' do 4 | let(:text) { '太丸ゴシック体' } 5 | let(:field) { TTY::Table::Field.new(text) } 6 | 7 | subject(:operation) { described_class.new(column_widths) } 8 | 9 | context 'without column width' do 10 | let(:column_widths) { [] } 11 | 12 | it "truncates string" do 13 | expect(operation.call(field, 0, 0)).to eql(text) 14 | end 15 | end 16 | 17 | context 'with column width ' do 18 | let(:column_widths) { [6, 8] } 19 | 20 | it "truncates string for 0 column" do 21 | expect(operation.call(field, 0, 0)).to eql('太丸…') 22 | end 23 | 24 | it "truncates string for 1 column" do 25 | expect(operation.call(field, 0, 1)).to eql('太丸ゴ…') 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/row/each_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#new' do 4 | let(:yields) { [] } 5 | let(:value) { 'a1' } 6 | let(:header) { ['Header1']} 7 | let(:row) { [ value ] } 8 | let(:object) { described_class.new row, header } 9 | 10 | context 'with block' do 11 | subject { object.each { |field| yields << field } } 12 | 13 | it 'yields only fields' do 14 | subject 15 | yields.each { |field| expect(field).to be_instance_of(value.class) } 16 | end 17 | 18 | it 'yields rows with expected attributes' do 19 | subject 20 | yields.each { |field| expect(field).to eql(value) } 21 | end 22 | 23 | it 'yields each row' do 24 | expect { subject }.to change { yields }. 25 | from( [] ). 26 | to( yields ) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/extract_column_widths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, '#column_widths' do 4 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 5 | let(:renderer) { described_class.new(table) } 6 | 7 | subject { renderer.render } 8 | 9 | context 'with rows only' do 10 | let(:rows) { [['a1a', 'a2a2a2'], ['b1b1b', 'b2b2']] } 11 | let(:table) { TTY::Table.new rows } 12 | 13 | it 'calculates column widths' do 14 | expect(renderer.column_widths).to eq([5,6]) 15 | end 16 | end 17 | 18 | context 'with header' do 19 | let(:header) { ['header1', 'head2', 'h3'] } 20 | let(:table) { TTY::Table.new header, rows } 21 | 22 | it 'calcualtes column widths with header' do 23 | expect(renderer.column_widths).to eq([7,5,2]) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tty/table/border/unicode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../border" 4 | 5 | module TTY 6 | class Table 7 | class Border 8 | # A class that represents a unicode border. 9 | # 10 | # @api private 11 | class Unicode < Border 12 | 13 | def_border do 14 | top "─" 15 | top_mid "┬" 16 | top_left "┌" 17 | top_right "┐" 18 | bottom "─" 19 | bottom_mid "┴" 20 | bottom_left "└" 21 | bottom_right "┘" 22 | mid "─" 23 | mid_mid "┼" 24 | mid_left "├" 25 | mid_right "┤" 26 | left "│" 27 | center "│" 28 | right "│" 29 | end 30 | 31 | end # Unicode 32 | end # Border 33 | end # Table 34 | end # TTY 35 | -------------------------------------------------------------------------------- /lib/tty/table/border/ascii.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../border" 4 | 5 | module TTY 6 | class Table 7 | class Border 8 | # A class that represents an ascii border. 9 | # 10 | # @api private 11 | class ASCII < Border 12 | 13 | def_border do 14 | top "-" 15 | top_mid "+" 16 | top_left "+" 17 | top_right "+" 18 | bottom "-" 19 | bottom_mid "+" 20 | bottom_left "+" 21 | bottom_right "+" 22 | mid "-" 23 | mid_mid "+" 24 | mid_left "+" 25 | mid_right "+" 26 | left "|" 27 | center "|" 28 | right "|" 29 | end 30 | 31 | end # ASCII 32 | end # Border 33 | end # Table 34 | end # TTY 35 | -------------------------------------------------------------------------------- /lib/tty/table/operation/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | module Operation 6 | # A class responsible for transforming table field 7 | # 8 | # @api private 9 | class Filter 10 | # Initialize a Filter 11 | # 12 | # @api public 13 | def initialize(filter) 14 | @filter = filter 15 | end 16 | 17 | # Apply filer to the provided table field 18 | # 19 | # @param [TTY::Table::Field] field 20 | # 21 | # @param [Integer] row 22 | # the field row index 23 | # 24 | # @param [Integer] col 25 | # the field column index 26 | # 27 | # @api public 28 | def call(field, row, col) 29 | @filter.call(field.content, row, col) 30 | end 31 | end # Filter 32 | end # Operation 33 | end # Table 34 | end # TTY 35 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: disable 3 | 4 | Lint/AssignmentInCondition: 5 | Enabled: false 6 | 7 | Metrics/AbcSize: 8 | Max: 30 9 | 10 | Metrics/BlockLength: 11 | CountComments: true 12 | Max: 25 13 | ExcludedMethods: [] 14 | Exclude: 15 | - "spec/**/*" 16 | 17 | Metrics/ClassLength: 18 | Max: 1500 19 | 20 | Metrics/CyclomaticComplexity: 21 | Enabled: false 22 | 23 | Layout/LineLength: 24 | Max: 80 25 | 26 | Metrics/MethodLength: 27 | Max: 20 28 | 29 | Naming/BinaryOperatorParameterName: 30 | Enabled: false 31 | 32 | Style/AsciiComments: 33 | Enabled: false 34 | 35 | Style/LambdaCall: 36 | SupportedStyles: 37 | - call 38 | - braces 39 | 40 | Style/StringLiterals: 41 | EnforcedStyle: double_quotes 42 | 43 | Style/TrivialAccessors: 44 | Enabled: false 45 | 46 | # { ... } for multi-line blocks is okay 47 | Style/BlockDelimiters: 48 | Enabled: false 49 | 50 | Style/CommentedKeyword: 51 | Enabled: false 52 | -------------------------------------------------------------------------------- /spec/unit/utf_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'unicode support' do 4 | it "measures utf characters correctly for :basic" do 5 | table = TTY::Table[['こんにちは', 'a2'], ['b1','選択']] 6 | expect(table.render(:basic)).to eq unindent(<<-EOS) 7 | こんにちは a2 8 | b1 選択 9 | EOS 10 | end 11 | 12 | it "measure utf characters correctly for :ascii" do 13 | table = TTY::Table[['こんにちは', 'a2'], ['b1','選択']] 14 | expect(table.render(:ascii)).to eq unindent(<<-EOS) 15 | +----------+----+ 16 | |こんにちは|a2 | 17 | |b1 |選択| 18 | +----------+----+ 19 | EOS 20 | end 21 | 22 | it "measure utf characters correctly for :unicode" do 23 | table = TTY::Table[['こんにちは', 'a2'], ['b1','選択']] 24 | expect(table.render(:unicode)).to eq unindent(<<-EOS) 25 | ┌──────────┬────┐ 26 | │こんにちは│a2 │ 27 | │b1 │選択│ 28 | └──────────┴────┘ 29 | EOS 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/validatable/validate_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Validatable, '#validate_options!' do 4 | let(:described_class) { Class.new { include TTY::Table::Validatable } } 5 | 6 | subject { described_class.new.validate_options! options } 7 | 8 | context 'with empty rows' do 9 | let(:options) { {rows: []} } 10 | 11 | it { expect { subject }.not_to raise_error() } 12 | end 13 | 14 | context 'with invalid rows type' do 15 | let(:options) { {rows: 1 } } 16 | 17 | it { expect { subject }.to raise_error(TTY::Table::InvalidArgument) } 18 | end 19 | 20 | context 'with empty header' do 21 | let(:options) { {header: []} } 22 | 23 | it { expect { subject }.to raise_error(TTY::Table::InvalidArgument) } 24 | end 25 | 26 | context 'with invalid header type' do 27 | let(:options) { {header: 1} } 28 | 29 | it { expect { subject }.to raise_error(TTY::Table::InvalidArgument) } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "rspec/core/rake_task" 5 | 6 | desc "Run all specs" 7 | RSpec::Core::RakeTask.new(:spec) do |task| 8 | task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb" 9 | end 10 | 11 | namespace :spec do 12 | desc "Run unit specs" 13 | RSpec::Core::RakeTask.new(:unit) do |task| 14 | task.pattern = "spec/unit{,/*/**}/*_spec.rb" 15 | end 16 | 17 | desc "Run integration specs" 18 | RSpec::Core::RakeTask.new(:integration) do |task| 19 | task.pattern = "spec/integration{,/*/**}/*_spec.rb" 20 | end 21 | 22 | desc "Run perf specs" 23 | RSpec::Core::RakeTask.new(:perf) do |task| 24 | task.pattern = "spec/perf{,/*/**}/*_spec.rb" 25 | end 26 | end 27 | 28 | rescue LoadError 29 | %w[spec spec:unit spec:integration].each do |name| 30 | task name do 31 | $stderr.puts "In order to run #{name}, do `gem install rspec`" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/row/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#call' do 4 | let(:object) { described_class.new(data) } 5 | 6 | subject { object[attribute] } 7 | 8 | context 'when integer' do 9 | let(:data) { ['a', 'b'] } 10 | 11 | let(:attribute) { 1 } 12 | 13 | it { is_expected.to eql('b') } 14 | end 15 | 16 | context 'when symbol' do 17 | let(:data) { {:id => 1} } 18 | 19 | context 'when hash access' do 20 | let(:attribute) { :id } 21 | 22 | it { is_expected.to eql(1) } 23 | end 24 | 25 | context 'when array access' do 26 | let(:attribute) { 0 } 27 | 28 | it { is_expected.to eql(1) } 29 | end 30 | end 31 | 32 | context 'when unkown attribute' do 33 | let(:data) { {:id => 1} } 34 | 35 | let(:attribute) { :other } 36 | 37 | specify { 38 | expect { 39 | subject 40 | }.to raise_error(TTY::Table::UnknownAttributeError) 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/transformation/extract_tuples_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Transformation, '#extract_tuples' do 4 | let(:object) { described_class } 5 | let(:header) { ['Header1', 'Header2'] } 6 | let(:rows) { [['a1', 'a2'], ['b1', 'b2']] } 7 | 8 | subject { object.extract_tuples(value) } 9 | 10 | context 'when rows' do 11 | let(:value) { [rows] } 12 | 13 | it { expect(subject[:header]).to be_nil } 14 | 15 | it { expect(subject[:rows]).to eql(rows) } 16 | end 17 | 18 | context 'when header and rows' do 19 | let(:value) { [header, rows] } 20 | 21 | it { expect(subject[:header]).to eql(header) } 22 | 23 | it { expect(subject[:rows]).to eql(rows) } 24 | end 25 | 26 | context 'when hash' do 27 | let(:value) { [[{'Header1' => ['a1', 'a2'], 'Header2' => ['b1', 'b2'] }]] } 28 | 29 | it { expect(subject[:header]).to eql(header) } 30 | 31 | it { expect(subject[:rows]).to eql(rows) } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/border/options/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::BorderOptions, "#new" do 4 | it "defaults characters to an empty hash" do 5 | expect(described_class.new.characters).to eq({}) 6 | end 7 | 8 | it "sets characters option" do 9 | border_options = described_class.new(characters: {top: "**"}) 10 | expect(border_options.characters).to eq({top: "**"}) 11 | end 12 | 13 | it "defaulats separator to nil" do 14 | expect(described_class.new.separator).to eq(nil) 15 | end 16 | 17 | it "sets separator to a value" do 18 | border_options = described_class.new(separator: :each_row) 19 | expect(border_options.separator).to eq(:each_row) 20 | end 21 | 22 | it "defaults border style to nil" do 23 | expect(described_class.new.style).to eq(nil) 24 | end 25 | 26 | it "sets border style to a value" do 27 | border_options = described_class.new(style: :red) 28 | expect(border_options.style).to eq(:red) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/row/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#new' do 4 | let(:object) { described_class } 5 | 6 | subject { object.new data } 7 | 8 | context 'with no arguments' do 9 | let(:data) { [] } 10 | 11 | it { is_expected.to be_instance_of(object) } 12 | 13 | it { is_expected.to be_empty } 14 | 15 | it { expect(subject.attributes).to eq([]) } 16 | 17 | it { expect(subject.data).to eq({}) } 18 | end 19 | 20 | context 'with Array argument' do 21 | let(:data) { ['a'] } 22 | 23 | it { is_expected.to be_instance_of(object) } 24 | 25 | it { expect(subject.attributes).to eq([0]) } 26 | 27 | it { expect(subject.to_hash).to eq({0 => "a"}) } 28 | end 29 | 30 | context 'with Hash argument' do 31 | let(:data) { {id: 'a'} } 32 | 33 | it { should be_instance_of(object) } 34 | 35 | it { expect(subject.attributes).to eq([:id]) } 36 | 37 | it { expect(subject.to_hash).to eq({:id => 'a'}) } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tty/table/operation/padding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "strings" 4 | 5 | module TTY 6 | class Table 7 | module Operation 8 | # A class responsible for padding field with whitespace 9 | # 10 | # Used internally by {Table::Renderer} 11 | class Padding 12 | # Initialize a Padding operation 13 | # 14 | # @param [Strings::Padder] padding 15 | # 16 | # @api public 17 | def initialize(padding) 18 | @padding = padding 19 | end 20 | 21 | # Apply padding to a field 22 | # 23 | # @param [TTY::Table::Field] field 24 | # the table field 25 | # 26 | # @return [TTY::Table::Field] 27 | # 28 | # @api public 29 | def call(field, *) 30 | Strings.pad(field.content, padding) 31 | end 32 | 33 | protected 34 | 35 | attr_reader :padding 36 | end # Padding 37 | end # Operation 38 | end # Table 39 | end # TTY 40 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/indentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, 'indentation' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | let(:indent) { 2 } 8 | let(:options) { {indent: indent } } 9 | 10 | subject(:renderer) { described_class.new(table, options)} 11 | 12 | context 'when default' do 13 | it 'indents by value' do 14 | expect(renderer.render).to eql <<-EOS.chomp 15 | +--+--+--+ 16 | |h1|h2|h3| 17 | +--+--+--+ 18 | |a1|a2|a3| 19 | |b1|b2|b3| 20 | +--+--+--+ 21 | EOS 22 | end 23 | end 24 | 25 | context 'when each row' do 26 | it 'indents by value' do 27 | renderer.border.separator = :each_row 28 | expect(renderer.render).to eql <<-EOS.chomp 29 | +--+--+--+ 30 | |h1|h2|h3| 31 | +--+--+--+ 32 | |a1|a2|a3| 33 | +--+--+--+ 34 | |b1|b2|b3| 35 | +--+--+--+ 36 | EOS 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/unit/renderer/unicode/indentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Unicode, 'indentation' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | let(:indent) { 2 } 8 | let(:options) { {indent: indent } } 9 | 10 | subject(:renderer) { described_class.new(table, options)} 11 | 12 | context 'when default' do 13 | it 'indents by value' do 14 | expect(renderer.render).to eq <<-EOS.chomp 15 | ┌──┬──┬──┐ 16 | │h1│h2│h3│ 17 | ├──┼──┼──┤ 18 | │a1│a2│a3│ 19 | │b1│b2│b3│ 20 | └──┴──┴──┘ 21 | EOS 22 | end 23 | end 24 | 25 | context 'when each row' do 26 | it 'indents by value' do 27 | renderer.border.separator = :each_row 28 | expect(renderer.render).to eql <<-EOS.chomp 29 | ┌──┬──┬──┐ 30 | │h1│h2│h3│ 31 | ├──┼──┼──┤ 32 | │a1│a2│a3│ 33 | ├──┼──┼──┤ 34 | │b1│b2│b3│ 35 | └──┴──┴──┘ 36 | EOS 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/unit/renderer/unicode/separator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Unicode, 'with separator' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | let(:object) { described_class.new table } 9 | 10 | subject(:renderer) { object } 11 | 12 | it "renders each row" do 13 | renderer.border.separator= :each_row 14 | expect(renderer.render).to eq unindent(<<-EOS) 15 | ┌──┬──┬──┐ 16 | │h1│h2│h3│ 17 | ├──┼──┼──┤ 18 | │a1│a2│a3│ 19 | ├──┼──┼──┤ 20 | │b1│b2│b3│ 21 | └──┴──┴──┘ 22 | EOS 23 | end 24 | 25 | it "will not the default separator if individual separators are specified" do 26 | renderer.border.separator = [1] 27 | expect(renderer.render).to eq unindent(<<-EOS) 28 | ┌──┬──┬──┐ 29 | │h1│h2│h3│ 30 | │a1│a2│a3│ 31 | ├──┼──┼──┤ 32 | │b1│b2│b3│ 33 | └──┴──┴──┘ 34 | EOS 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/validatable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Validatable do 4 | let(:described_class) { Class.new { include TTY::Table::Validatable } } 5 | let(:rows) { [['a1', 'a2'], ['b1']] } 6 | 7 | subject { described_class.new } 8 | 9 | it 'raises no exception' do 10 | rows[1] << ['b2'] 11 | expect { subject.assert_row_sizes(rows) }.not_to raise_error 12 | end 13 | 14 | it 'raises exception for mismatched rows' do 15 | expect { subject.assert_row_sizes(rows) }. 16 | to raise_error(TTY::Table::DimensionMismatchError) 17 | end 18 | 19 | it "raises exception when :header key has wrong data type" do 20 | options = {header: 'h1'} 21 | expect { subject.validate_options!(options) }. 22 | to raise_error(TTY::Table::InvalidArgument) 23 | end 24 | 25 | it "raises exception when :rows key has wrong data type" do 26 | options = {rows: 'a1'} 27 | expect { subject.validate_options!(options) }. 28 | to raise_error(TTY::Table::InvalidArgument) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/field/length_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Field, '.length' do 4 | it "calculates length for nil string" do 5 | field = described_class.new(nil) 6 | expect(field.length).to eq(0) 7 | end 8 | 9 | it "calculates length for empty string" do 10 | field = described_class.new('') 11 | expect(field.length).to eq(0) 12 | end 13 | 14 | it "calculates maximum length for multiline string" do 15 | field = described_class.new("Multi\nLine\nContent") 16 | expect(field.length).to eq(7) 17 | end 18 | 19 | it "calculates length for unicode string" do 20 | field = described_class.new('こんにちは') 21 | expect(field.length).to eq(10) 22 | end 23 | 24 | it "calculates length for escaped string" do 25 | field = described_class.new("Multi\\nLine") 26 | expect(field.length).to eq(11) 27 | end 28 | 29 | it "calculates length for colored string" do 30 | field = described_class.new("\e[32;41mgreen on red\e[0m") 31 | expect(field.length).to eq(12) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/operation/wrapped/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Operation::Wrapped, '#call' do 4 | let(:text) { 'ラドクリフ、マラソン五輪代表に1万m出場にも含み' } 5 | let(:field) { TTY::Table::Field.new(text) } 6 | 7 | subject(:operation) { described_class.new(column_widths) } 8 | 9 | context 'without column width' do 10 | let(:column_widths) { [] } 11 | 12 | it "doesn't wrap string" do 13 | expect(operation.call(field, 0, 0)).to eql(text) 14 | end 15 | end 16 | 17 | context 'with column width' do 18 | let(:column_widths) { [12, 14] } 19 | 20 | it "wraps string for 0 column" do 21 | expect(operation.call(field, 0, 0)).to eql([ 22 | "ラドクリフ、", 23 | "マラソン五輪", 24 | "代表に1万m出", 25 | "場にも含み" 26 | ].join("\n")) 27 | end 28 | 29 | it "wraps string for 1 column" do 30 | expect(operation.call(field, 0, 1)).to eql([ 31 | "ラドクリフ、マ", 32 | "ラソン五輪代表", 33 | "に1万m出場にも", 34 | "含み" 35 | ].join("\n")) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/truncation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'truncation' do 4 | let(:header) { ['header1', 'head2', 'h3'] } 5 | let(:rows) { [['a1111111', 'a222', 'a3333333'], ['b111', 'b2222222', 'b333333']]} 6 | let(:table) { TTY::Table.new header, rows } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'without column widths' do 11 | let(:options) { {} } 12 | 13 | it "doesn't shorten the fields" do 14 | expect(renderer.render).to eq unindent(<<-EOS) 15 | header1 head2 h3 16 | a1111111 a222 a3333333 17 | b111 b2222222 b333333 18 | EOS 19 | end 20 | end 21 | 22 | context 'with column widths' do 23 | let(:options) { { column_widths: [4, 5, 7] } } 24 | 25 | it 'shortens the fields' do 26 | expect(renderer.render).to eq unindent(<<-EOS) 27 | he… head2 h3 28 | a1… a222 a3333… 29 | b111 b22… b333333 30 | EOS 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/border/options/from_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::BorderOptions, "#from" do 4 | 5 | subject(:options) { described_class.from object } 6 | 7 | context "when empty hash" do 8 | let(:object) { {} } 9 | 10 | it { expect(options.style).to be_nil } 11 | 12 | it { expect(options.separator).to be_nil } 13 | end 14 | 15 | context "when hash" do 16 | let(:object) { { style: :red, separator: :none } } 17 | 18 | it { expect(options).to be_kind_of(described_class) } 19 | 20 | it { expect(options.style).to eql :red } 21 | 22 | it { expect(options.separator).to eql :none } 23 | 24 | it { expect(options.characters).to eql({}) } 25 | end 26 | 27 | context "when BorderOptions" do 28 | let(:object) { described_class.new(style: :red, separator: :none) } 29 | 30 | it { expect(options).to be_kind_of(described_class) } 31 | 32 | it { expect(options.style).to eql :red } 33 | 34 | it { expect(options.separator).to eql :none } 35 | 36 | it { expect(options.characters).to eql({}) } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/column_constraint/widths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::ColumnConstraint, 'column widths' do 4 | let(:header) { ['h1', 'h2', 'h3', 'h4'] } 5 | let(:rows) { [['a1', 'a2', 'a3', 'a4'], ['b1', 'b2', 'b3', 'b4']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:columns) { described_class.new(table, renderer) } 9 | 10 | context 'with basic renderer' do 11 | let(:renderer) { TTY::Table::Renderer::Basic.new(table) } 12 | 13 | it 'calculates columns natural width' do 14 | expect(columns.natural_width).to eq(11) 15 | end 16 | 17 | it 'calculates miminimum columns width' do 18 | expect(columns.minimum_width).to eq(7) 19 | end 20 | end 21 | 22 | context 'with ascii renderer' do 23 | let(:renderer) { TTY::Table::Renderer::ASCII.new(table) } 24 | 25 | it 'calculates columns natural width' do 26 | expect(columns.natural_width).to eq(13) 27 | end 28 | 29 | it 'calculates miminimum columns width' do 30 | expect(columns.minimum_width).to eq(9) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/tty/table/border/null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../border" 4 | 5 | module TTY 6 | class Table 7 | class Border 8 | # A class that represents no border. 9 | class Null < Border 10 | def_border do 11 | top EMPTY_CHAR 12 | top_mid EMPTY_CHAR 13 | top_left EMPTY_CHAR 14 | top_right EMPTY_CHAR 15 | bottom EMPTY_CHAR 16 | bottom_mid EMPTY_CHAR 17 | bottom_left EMPTY_CHAR 18 | bottom_right EMPTY_CHAR 19 | mid EMPTY_CHAR 20 | mid_mid EMPTY_CHAR 21 | mid_left EMPTY_CHAR 22 | mid_right EMPTY_CHAR 23 | left EMPTY_CHAR 24 | center SPACE_CHAR 25 | right EMPTY_CHAR 26 | end 27 | 28 | # A stub middle line 29 | # 30 | # @api private 31 | def middle_line 32 | border_options.separator ? "" : super 33 | end 34 | end # Null 35 | end # Border 36 | end # Table 37 | end # TTY 38 | -------------------------------------------------------------------------------- /lib/tty/table/operation/wrapped.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "strings" 4 | 5 | module TTY 6 | class Table 7 | module Operation 8 | # A class responsible for wrapping text. 9 | # 10 | # @api private 11 | class Wrapped 12 | attr_reader :widths 13 | 14 | # Initialize a Wrapped 15 | # 16 | # @api public 17 | def initialize(widths) 18 | @widths = widths 19 | end 20 | 21 | # Apply wrapping to a field 22 | # 23 | # @param [TTY::Table::Field] field 24 | # the table field 25 | # 26 | # @param [Integer] row 27 | # the field row index 28 | # 29 | # @param [Integer] col 30 | # the field column index 31 | # 32 | # @return [Array[String]] 33 | # 34 | # @api public 35 | def call(field, row, col) 36 | column_width = widths[col] || field.width 37 | Strings.wrap(field.content, column_width) 38 | end 39 | end # Wrapped 40 | end # Operation 41 | end # Table 42 | end # TTY 43 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/separator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, 'with separator' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | let(:object) { described_class.new table } 9 | 10 | subject(:renderer) { object } 11 | 12 | context 'when ascii' do 13 | it "renders each row" do 14 | renderer.border.separator = :each_row 15 | expect(renderer.render).to eq unindent(<<-EOS) 16 | +--+--+--+ 17 | |h1|h2|h3| 18 | +--+--+--+ 19 | |a1|a2|a3| 20 | +--+--+--+ 21 | |b1|b2|b3| 22 | +--+--+--+ 23 | EOS 24 | end 25 | 26 | it "will not the default separator if individual separators are specified" do 27 | renderer.border.separator = [1] 28 | expect(renderer.render).to eq unindent(<<-EOS) 29 | +--+--+--+ 30 | |h1|h2|h3| 31 | |a1|a2|a3| 32 | +--+--+--+ 33 | |b1|b2|b3| 34 | +--+--+--+ 35 | EOS 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tty/table/operation/truncation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "strings" 4 | 5 | module TTY 6 | class Table 7 | module Operation 8 | # A class responsible for shortening text. 9 | # 10 | # @api private 11 | class Truncation 12 | 13 | attr_reader :widths 14 | 15 | # Initialize a Truncation 16 | # 17 | # @api public 18 | def initialize(widths) 19 | @widths = widths 20 | end 21 | 22 | # Apply truncation to a field 23 | # 24 | # @param [TTY::Table::Field] field 25 | # the table field 26 | # 27 | # @param [Integer] row 28 | # the field row index 29 | # 30 | # @param [Integer] col 31 | # the field column index 32 | # 33 | # @return [TTY::Table::Field] 34 | # 35 | # @api public 36 | def call(field, row, col) 37 | column_width = widths[col] || field.width 38 | Strings.truncate(field.content, column_width) 39 | end 40 | end # Truncation 41 | end # Operation 42 | end # Table 43 | end # TTY 44 | -------------------------------------------------------------------------------- /spec/unit/operation/alignment/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Operation::Alignment, '#call' do 4 | let(:object) { described_class.new alignments, widths } 5 | let(:value) { 'a1' } 6 | let(:field) { TTY::Table::Field.new(value)} 7 | 8 | subject { object.call(field, 0, 0) } 9 | 10 | context 'aligned with column widths and no alignments' do 11 | let(:alignments) { [] } 12 | let(:widths) { [4, 4] } 13 | 14 | it { is_expected.to eq("#{value} ") } 15 | end 16 | 17 | context 'aligned with column widths and alignments' do 18 | let(:alignments) { [:right, :left] } 19 | let(:widths) { [4, 4] } 20 | 21 | it { is_expected.to eq(" #{value}") } 22 | end 23 | 24 | context 'aligned with no column widths and no alignments' do 25 | let(:alignments) { [] } 26 | let(:widths) { [] } 27 | 28 | it { is_expected.to eq("#{value}") } 29 | end 30 | 31 | context 'aligned with no column widths and alignments' do 32 | let(:alignments) { [:right, :left] } 33 | let(:widths) { [] } 34 | 35 | it { is_expected.to eq("#{value}") } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /benchmarks/speed.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'benchmark' 4 | require 'benchmark/ips' 5 | 6 | require_relative '../lib/tty-table' 7 | 8 | header = [:name, :color] 9 | rows = (1..100).map { |n| ["row#{n}", "red"] } 10 | table = TTY::Table.new(header, rows) 11 | 12 | # Benchmark speed of table operations 13 | Benchmark.ips do |r| 14 | r.report("Ruby #to_s") do 15 | rows.to_s 16 | end 17 | 18 | r.report("TTY #render") do 19 | table.render 20 | end 21 | 22 | r.report("TTY #render ASCII") do 23 | table.render(:ascii) 24 | end 25 | 26 | r.report("TTY #render Unicode") do 27 | table.render(:unicode) 28 | end 29 | 30 | r.report("TTY #render Color") do 31 | table.render(:ascii, border: {style: :red}) 32 | end 33 | end 34 | 35 | # Ruby #to_s 2588.6 (±12.2%) i/s - 12948 in 5.084883s 36 | # TTY #render 20.8 (±9.6%) i/s - 104 in 5.030159s 37 | # TTY #render ASCII 18.1 (±16.5%) i/s - 89 in 5.041230s 38 | # TTY #render Unicode 18.0 (±16.7%) i/s - 88 in 5.029868s 39 | # TTY #render Color 11.7 (±17.1%) i/s - 58 in 5.071654s 40 | -------------------------------------------------------------------------------- /lib/tty/table/orientation/vertical.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | # A class representing table orientation 6 | class Orientation 7 | # A class responsible for vertical table transformation 8 | class Vertical < Orientation 9 | # Rotate table vertically 10 | # 11 | # @param [Table] table 12 | # 13 | # @return [nil] 14 | # 15 | # @api public 16 | def transform(table) 17 | table.rotate_vertical 18 | end 19 | 20 | # Slice horizontal table data into vertical 21 | # 22 | # @param [Table] table 23 | # 24 | # @api public 25 | def slice(table) 26 | header = table.header 27 | rows_size = table.rows_size 28 | 29 | head = header ? header : (0..rows_size).map { |n| (n + 1).to_s } 30 | 31 | (0...rows_size).reduce([]) do |array, index| 32 | array + head.zip(table.rows[index]).map { |row| table.to_row(row) } 33 | end 34 | end 35 | end # Vertical 36 | end # Orientation 37 | end # Table 38 | end # TTY 39 | -------------------------------------------------------------------------------- /lib/tty/table/transformation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | # A class for transforming table values 6 | # 7 | # Used internally by {Table} 8 | # 9 | # @api private 10 | class Transformation 11 | # Extract the header and row tuples from the value 12 | # 13 | # @param [Array] args 14 | # 15 | # @return [Object] 16 | # 17 | # @api public 18 | def self.extract_tuples(args) 19 | rows = args.pop 20 | header = args.size.zero? ? nil : args.first 21 | if rows.first.is_a?(Hash) 22 | header, rows = group_header_and_rows(rows) 23 | end 24 | { header: header, rows: rows } 25 | end 26 | 27 | # Group hash keys into header and values into rows 28 | # 29 | # @param [Hash] value 30 | # 31 | # @api public 32 | def self.group_header_and_rows(value) 33 | header = value.map(&:keys).flatten.uniq 34 | rows = value.reduce([]) { |arr, el| arr + el.values } 35 | [header, rows] 36 | end 37 | end # Transformation 38 | end # Table 39 | end # TTY 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Piotr Murach (piotrmurach.com) 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/tty/table/indentation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | # A module responsible for indenting table representation 6 | module Indentation 7 | # Return a table part with indentation inserted 8 | # 9 | # @param [#map, #to_s] part 10 | # the rendered table part 11 | # 12 | # @api public 13 | def indent(part, indentation) 14 | if part.is_a?(Enumerable) && part.respond_to?(:to_a) 15 | part.map { |line| insert_indentation(line, indentation) } 16 | else 17 | insert_indentation(part, indentation) 18 | end 19 | end 20 | module_function :indent 21 | 22 | # Insert indentation into a table renderd line 23 | # 24 | # @param [String] line 25 | # the rendered table line 26 | # @param [Integer] indentation 27 | # the amount of indentation to apply 28 | # 29 | # @return [String] 30 | # 31 | # @api public 32 | def insert_indentation(line, indentation) 33 | line ? " " * indentation + line.to_s : "" 34 | end 35 | module_function :insert_indentation 36 | end # Indentation 37 | end # Table 38 | end # TTY 39 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/wrapping_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'wrapping' do 4 | let(:header) { ['header1', 'head2', 'h3'] } 5 | let(:rows) { [['a1111111', 'a222', 'a3333333'], ['b111', 'b2222222', 'b333333']]} 6 | let(:table) { TTY::Table.new header, rows } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'without column widths' do 11 | let(:options) { {multiline: true} } 12 | 13 | it "doesn't wrap the fields" do 14 | expect(renderer.render).to eq unindent(<<-EOS) 15 | header1 head2 h3 16 | a1111111 a222 a3333333 17 | b111 b2222222 b333333 18 | EOS 19 | end 20 | end 21 | 22 | context 'with column widths' do 23 | let(:options) { { column_widths: [3, 5, 7], multiline: true} } 24 | 25 | it 'wraps the fields' do 26 | expect(renderer.render).to eq unindent(<<-EOS) 27 | hea head2 h3 28 | der 29 | 1 30 | a11 a222 a333333 31 | 111 3 32 | 11 33 | b11 b2222 b333333 34 | 1 222 35 | EOS 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/renderer/render_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer, '#render' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject { described_class.render(table, {}, &block) } 9 | 10 | context 'when default' do 11 | let(:renderer) { double(:renderer).as_null_object } 12 | let(:renderer_class) { double(:renderer_class).as_null_object } 13 | let(:yielded) { [] } 14 | let(:block) { proc { |render| yielded << render } } 15 | 16 | before { allow(described_class).to receive(:select).and_return(renderer_class) } 17 | 18 | it 'creates renderer' do 19 | subject 20 | expect(renderer_class).to have_received(:new).with(table, {}) 21 | end 22 | 23 | it 'yields renderer' do 24 | allow(renderer_class).to receive(:new).and_return(renderer) 25 | expect { subject }.to change { yielded}.from([]).to([renderer]) 26 | end 27 | 28 | it 'calls render' do 29 | allow(renderer_class).to receive(:new).and_return(renderer) 30 | subject 31 | expect(renderer).to have_received(:render) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/indentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'indentation' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | let(:options) { {indent: indent } } 8 | 9 | subject(:renderer) { described_class.new(table, options)} 10 | 11 | context 'when default' do 12 | let(:indent) { 0 } 13 | 14 | it 'indents by value' do 15 | expect(renderer.render).to eql <<-EOS.chomp 16 | h1 h2 h3 17 | a1 a2 a3 18 | b1 b2 b3 19 | EOS 20 | end 21 | end 22 | 23 | context 'when custom' do 24 | let(:indent) { 2 } 25 | 26 | it 'indents by value' do 27 | expect(renderer.render).to eql <<-EOS.chomp 28 | h1 h2 h3 29 | a1 a2 a3 30 | b1 b2 b3 31 | EOS 32 | end 33 | end 34 | 35 | context 'when changed' do 36 | let(:indent) { 2 } 37 | let(:header) { ['h1', 'h2'] } 38 | let(:rows) { [['a1', 'a2']] } 39 | 40 | it 'changes indentation and reuses renderer' do 41 | expect(renderer.render).to eq(" h1 h2\n a1 a2") 42 | renderer.indent = 1 43 | expect(renderer.render).to eq(" h1 h2\n a1 a2") 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/unit/render_repeat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'render repeat' do 4 | it "renders consistently" do 5 | table = TTY::Table.new header: ['header1', 'header2'] 6 | table << ['a1', 'a2'] 7 | table << ['b1', 'b2'] 8 | options = {padding: [1,1,1,1], alignments: [:right, :center]} 9 | expect(table.render(:ascii, options)).to eq unindent(<<-EOS) 10 | +---------+---------+ 11 | | | | 12 | | header1 | header2 | 13 | | | | 14 | +---------+---------+ 15 | | | | 16 | | a1 | a2 | 17 | | | | 18 | | | | 19 | | b1 | b2 | 20 | | | | 21 | +---------+---------+ 22 | EOS 23 | 24 | expect(table.render(:ascii, options)).to eq unindent(<<-EOS) 25 | +---------+---------+ 26 | | | | 27 | | header1 | header2 | 28 | | | | 29 | +---------+---------+ 30 | | | | 31 | | a1 | a2 | 32 | | | | 33 | | | | 34 | | b1 | b2 | 35 | | | | 36 | +---------+---------+ 37 | EOS 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tty/table/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | # Raised when inserting into table with a mismatching row(s) 6 | class DimensionMismatchError < ArgumentError; end 7 | 8 | # Raised when reading non-existent element from a table 9 | class TupleMissing < IndexError 10 | attr_reader :i, :j 11 | 12 | def initialize(i, j) 13 | @i, @j = i, j 14 | super("element at(#{i},#{j}) not found") 15 | end 16 | end 17 | 18 | # Raised when the table orientation is unkown 19 | class InvalidOrientationError < ArgumentError; end 20 | 21 | # Raised when the table cannot be resized 22 | class ResizeError < ArgumentError; end 23 | 24 | # Raised when the operation is not implemented 25 | class NoImplementationError < NotImplementedError; end 26 | 27 | # Raised when the argument type is different from expected 28 | class TypeError < ArgumentError; end 29 | 30 | # Raised when the required argument is not supplied 31 | class ArgumentRequired < ArgumentError; end 32 | 33 | # Raised when the argument is not expected 34 | class InvalidArgument < ArgumentError; end 35 | 36 | # Raised when the attribute is unknown 37 | class UnknownAttributeError < IndexError; end 38 | end # Table 39 | end # TTY 40 | -------------------------------------------------------------------------------- /spec/unit/header/equality_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Header, '#==' do 4 | let(:attributes) { [:id, :name] } 5 | let(:object) { described_class.new(attributes) } 6 | 7 | subject { object == other } 8 | 9 | context 'with the same object' do 10 | let(:other) { object } 11 | 12 | it { is_expected.to eql(true) } 13 | 14 | it 'is symmetric' do 15 | is_expected.to eql(other == object) 16 | end 17 | end 18 | 19 | context 'with an equivalent object' do 20 | let(:other) { object.dup } 21 | 22 | it { is_expected.to eql(true) } 23 | 24 | it 'is symmetric' do 25 | is_expected.to eql(other == object) 26 | end 27 | end 28 | 29 | context 'with an equivalent object of subclass' do 30 | let(:other) { Class.new(described_class).new(attributes) } 31 | 32 | it { is_expected.to eql(true) } 33 | 34 | it 'is symmetric' do 35 | is_expected.to eql(other == object) 36 | end 37 | end 38 | 39 | context 'with an object having different attributes' do 40 | let(:other_attributes) { [:text] } 41 | let(:other) { described_class.new(other_attributes) } 42 | 43 | it { is_expected.to eql(false) } 44 | 45 | it 'is symmetric' do 46 | is_expected.to eql(other == object) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/unit/columns/widths_from_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Columns, '#widths_from' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new header, rows } 7 | 8 | subject { described_class.widths_from(table, column_widths) } 9 | 10 | context 'when empty array' do 11 | let(:column_widths) { [] } 12 | 13 | it 'raises an error' do 14 | expect { subject }.to raise_error(TTY::Table::InvalidArgument) 15 | end 16 | end 17 | 18 | context 'when invalid size array' do 19 | let(:column_widths) { [3,3] } 20 | 21 | it 'raises an error' do 22 | expect { subject }.to raise_error(TTY::Table::InvalidArgument) 23 | end 24 | end 25 | 26 | context 'when valid array' do 27 | let(:column_widths) { [3,3,3] } 28 | 29 | it 'converts into numbers' do 30 | expect(subject).to eql(column_widths) 31 | end 32 | end 33 | 34 | context 'when nil' do 35 | let(:column_widths) { nil } 36 | 37 | it 'extracts widths' do 38 | expect(subject).to eql([2,2,2]) 39 | end 40 | end 41 | 42 | context 'when numeric' do 43 | let(:column_widths) { 5 } 44 | 45 | it 'generates widths' do 46 | expect(subject).to eql([5,5,5]) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV["COVERAGE"] == "true" 4 | require "simplecov" 5 | require "coveralls" 6 | 7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 8 | SimpleCov::Formatter::HTMLFormatter, 9 | Coveralls::SimpleCov::Formatter 10 | ]) 11 | 12 | SimpleCov.start do 13 | command_name "spec" 14 | add_filter "spec" 15 | end 16 | end 17 | 18 | require "tty-table" 19 | 20 | RSpec.configure do |config| 21 | config.expect_with :rspec do |expectations| 22 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 23 | end 24 | 25 | config.mock_with :rspec do |mocks| 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 30 | config.disable_monkey_patching! 31 | 32 | # This setting enables warnings. It's recommended, but in some cases may 33 | # be too noisy due to issues in dependencies. 34 | config.warnings = true 35 | 36 | if config.files_to_run.one? 37 | config.default_formatter = "doc" 38 | end 39 | 40 | config.profile_examples = 2 41 | 42 | config.order = :random 43 | 44 | Kernel.srand config.seed 45 | end 46 | 47 | def unindent(string) 48 | prefix = string.scan(/^[ \t]+(?=\S)/).min 49 | string.gsub(/^#{prefix}/, "").chomp 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'options' do 4 | let(:rows) { [['a1', 'a2'], ['b1', 'b2']] } 5 | let(:object) { described_class } 6 | let(:table) { TTY::Table.new(rows) } 7 | let(:widths) { nil } 8 | let(:alignments) { [] } 9 | let(:options) { 10 | { 11 | column_widths: widths, 12 | alignments: alignments, 13 | renderer: :basic 14 | } 15 | } 16 | 17 | subject(:renderer) { object.new table, options } 18 | 19 | it { expect(renderer.border).to be_kind_of(TTY::Table::BorderOptions) } 20 | 21 | it { expect(renderer.column_widths).to eql([2,2]) } 22 | 23 | it { expect(renderer.alignments.to_a).to eql(alignments) } 24 | 25 | it { expect(renderer.alignments.to_a).to be_empty } 26 | 27 | context '#column_widths' do 28 | let(:widths) { [10, 10] } 29 | 30 | it { expect(renderer.column_widths).to eq(widths) } 31 | end 32 | 33 | context '#column_widths empty' do 34 | let(:widths) { [] } 35 | 36 | it { 37 | expect { 38 | renderer.column_widths 39 | }.to raise_error(TTY::Table::InvalidArgument) 40 | } 41 | end 42 | 43 | context '#alignments' do 44 | let(:alignments) { [:center, :center] } 45 | 46 | it 'unwraps original array' do 47 | expect(renderer.alignments.to_a).to eq(alignments) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/to_s_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe TTY::Table, '#to_s' do 6 | let(:header) { ['h1', 'h2', 'h3'] } 7 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 8 | 9 | subject(:table) { described_class.new(header, rows) } 10 | 11 | context 'without renderer' do 12 | it 'displayes basic table' do 13 | expect(table.render(:basic)).to eq unindent(<<-EOS) 14 | h1 h2 h3 15 | a1 a2 a3 16 | b1 b2 b3 17 | EOS 18 | end 19 | end 20 | 21 | context 'without border' do 22 | it 'displays table' do 23 | expect(table.to_s).to eq unindent(<<-EOS) 24 | h1 h2 h3 25 | a1 a2 a3 26 | b1 b2 b3 27 | EOS 28 | end 29 | end 30 | 31 | context 'with ascii border' do 32 | it 'displays table' do 33 | expect(table.render(:ascii)).to eq unindent(<<-EOS) 34 | +--+--+--+ 35 | |h1|h2|h3| 36 | +--+--+--+ 37 | |a1|a2|a3| 38 | |b1|b2|b3| 39 | +--+--+--+ 40 | EOS 41 | end 42 | end 43 | 44 | context 'with unicode border' do 45 | it 'displays table' do 46 | expect(table.render(:unicode)).to eq unindent(<<-EOS) 47 | ┌──┬──┬──┐ 48 | │h1│h2│h3│ 49 | ├──┼──┼──┤ 50 | │a1│a2│a3│ 51 | │b1│b2│b3│ 52 | └──┴──┴──┘ 53 | EOS 54 | end 55 | end 56 | end # to_s 57 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/separator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, "with separator" do 4 | let(:header) { ["h1", "h2", "h3"] } 5 | let(:rows) { [%w[a1 a2 a3], %w[b1 b2 b3], %w[c1 c2 c3]] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | let(:object) { described_class.new table } 9 | 10 | subject(:renderer) { object } 11 | 12 | context "when default" do 13 | it "sets through hash" do 14 | renderer.border :separator => :each_row 15 | expect(renderer.border.separator).to eql(:each_row) 16 | end 17 | 18 | it "sets through attribute" do 19 | renderer.border.separator= :each_row 20 | expect(renderer.border.separator).to eql(:each_row) 21 | end 22 | 23 | it "sets through block" do 24 | renderer.border do 25 | separator :each_row 26 | end 27 | expect(renderer.border.separator).to eql(:each_row) 28 | end 29 | 30 | it "renders without any separator" do 31 | expect(renderer.render).to eq unindent(<<-EOS) 32 | h1 h2 h3 33 | a1 a2 a3 34 | b1 b2 b3 35 | c1 c2 c3 36 | EOS 37 | end 38 | 39 | it "renders separating each row" do 40 | renderer.border.separator= :each_row 41 | expect(renderer.render).to eq unindent(<<-EOS) 42 | h1 h2 h3 43 | 44 | a1 a2 a3 45 | 46 | b1 b2 b3 47 | 48 | c1 c2 c3 49 | EOS 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/tty/table/orientation/horizontal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | # A class representing table orientation 6 | class Orientation 7 | # A class responsible for horizontal table transformation 8 | class Horizontal < Orientation 9 | # Rotate table horizontally 10 | # 11 | # @param [Table] table 12 | # 13 | # @return [nil] 14 | # 15 | # @api public 16 | def transform(table) 17 | table.rotate_horizontal 18 | end 19 | 20 | # Slice vertical table data into horizontal 21 | # 22 | # @param [Table] table 23 | # 24 | # @api public 25 | def slice(table) 26 | head, body, array_h, array_b = 4.times.map { [] } 27 | index = 0 28 | first_column = 0 29 | second_column = 1 30 | 31 | (0...table.original_columns * table.original_rows).each do |col_index| 32 | row = table.rows[index] 33 | array_h += [row[first_column]] 34 | array_b += [row[second_column]] 35 | 36 | if col_index % table.original_columns == 2 37 | head << array_h 38 | body << array_b 39 | array_h, array_b = [], [] 40 | end 41 | index += 1 42 | end 43 | [head, body] 44 | end 45 | end # Horizontal 46 | end # Orientation 47 | end # Table 48 | end # TTY 49 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'filter' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:renderer) { described_class.new(table, {filter: filter}) } 9 | 10 | context 'with header' do 11 | context 'filtering only rows' do 12 | let(:filter) { Proc.new { |val, row, col| 13 | (col == 1 and row > 0) ? val.capitalize : val 14 | } 15 | } 16 | 17 | it 'filters only rows' do 18 | expect(renderer.render).to eq unindent(<<-EOS) 19 | h1 h2 h3 20 | a1 A2 a3 21 | b1 B2 b3 22 | EOS 23 | end 24 | end 25 | 26 | context 'filtering header and rows' do 27 | let(:filter) { Proc.new { |val, row, col| col == 1 ? val.capitalize : val }} 28 | 29 | it 'filters only rows' do 30 | expect(renderer.render).to eq unindent(<<-EOS) 31 | h1 H2 h3 32 | a1 A2 a3 33 | b1 B2 b3 34 | EOS 35 | end 36 | end 37 | end 38 | 39 | context 'without header' do 40 | let(:header) { nil } 41 | 42 | let(:filter) { Proc.new { |val, row, col| col == 1 ? val.capitalize : val } } 43 | 44 | it 'filters only rows' do 45 | expect(renderer.render).to eq unindent(<<-EOS) 46 | a1 A2 a3 47 | b1 B2 b3 48 | EOS 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/tty/table/orientation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "orientation/horizontal" 4 | require_relative "orientation/vertical" 5 | 6 | module TTY 7 | class Table 8 | # A class representing table orientation 9 | # 10 | # @api private 11 | class Orientation 12 | # The name for the orientation 13 | # 14 | # @api public 15 | attr_reader :name 16 | 17 | # Initialize an Orientation 18 | # 19 | # @api public 20 | def initialize(name) 21 | @name = name 22 | end 23 | 24 | # Coerce the name argument into an orientation 25 | # 26 | # @param [Symbol] name 27 | # 28 | # @api public 29 | def self.coerce(name) 30 | case name.to_s 31 | when /h|horiz(ontal)?/i 32 | Horizontal.new :horizontal 33 | when /v|ert(ical)?/i 34 | Vertical.new :vertical 35 | else 36 | raise InvalidOrientationError, 37 | "orientation must be one of :horizontal, :vertical" 38 | end 39 | end 40 | 41 | # Check if orientation is vertical 42 | # 43 | # @return [Boolean] 44 | # 45 | # @api public 46 | def vertical? 47 | name == :vertical 48 | end 49 | 50 | # Check if orientation is horizontal 51 | # 52 | # @return [Boolean] 53 | # 54 | # @api public 55 | def horizontal? 56 | name == :horizontal 57 | end 58 | end # Orientation 59 | end # Table 60 | end # TTY 61 | -------------------------------------------------------------------------------- /tty-table.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/tty/table/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "tty-table" 7 | spec.version = TTY::Table::VERSION 8 | spec.authors = ["Piotr Murach"] 9 | spec.email = ["piotr@piotrmurach.com"] 10 | spec.summary = %q{A flexible and intuitive table generator} 11 | spec.description = %q{A flexible and intuitive table generator} 12 | spec.homepage = "https://ttytoolkit.org" 13 | spec.license = "MIT" 14 | if spec.respond_to?(:metadata=) 15 | spec.metadata = { 16 | "allowed_push_host" => "https://rubygems.org", 17 | "bug_tracker_uri" => "https://github.com/piotrmurach/tty-table/issues", 18 | "changelog_uri" => "https://github.com/piotrmurach/tty-table/blob/master/CHANGELOG.md", 19 | "documentation_uri" => "https://www.rubydoc.info/gems/tty-table", 20 | "homepage_uri" => spec.homepage, 21 | "source_code_uri" => "https://github.com/piotrmurach/tty-table" 22 | } 23 | end 24 | spec.files = Dir["lib/**/*"] 25 | spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE.txt"] 26 | spec.require_paths = ["lib"] 27 | spec.required_ruby_version = ">= 2.0.0" 28 | 29 | spec.add_dependency "pastel", "~> 0.8" 30 | spec.add_dependency "strings", "~> 0.2.0" 31 | spec.add_dependency "tty-screen", "~> 0.8" 32 | 33 | spec.add_development_dependency "rake" 34 | spec.add_development_dependency "rspec", ">= 3.0" 35 | end 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "benchmarks/**" 9 | - "examples/**" 10 | - "*.md" 11 | pull_request: 12 | branches: 13 | - master 14 | paths-ignore: 15 | - "benchmarks/**" 16 | - "examples/**" 17 | - "*.md" 18 | jobs: 19 | tests: 20 | name: Ruby ${{ matrix.ruby }} 21 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | ruby: 26 | - "2.0" 27 | - "2.1" 28 | - "2.3" 29 | - "2.4" 30 | - "2.5" 31 | - "2.6" 32 | - "3.0" 33 | - "3.1" 34 | - "3.2" 35 | - "3.3" 36 | - ruby-head 37 | - jruby-9.2 38 | - jruby-9.3 39 | - jruby-9.4 40 | - jruby-head 41 | - truffleruby-head 42 | include: 43 | - ruby: "2.2" 44 | os: ubuntu-20.04 45 | - ruby: "2.7" 46 | coverage: true 47 | env: 48 | COVERAGE: ${{ matrix.coverage }} 49 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 50 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Set up Ruby 54 | uses: ruby/setup-ruby@v1 55 | with: 56 | ruby-version: ${{ matrix.ruby }} 57 | bundler-cache: true 58 | - name: Run tests 59 | run: bundle exec rake ci 60 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/render_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, '#render' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new header, rows } 7 | 8 | subject(:renderer) { described_class.new(table) } 9 | 10 | context 'with rows' do 11 | let(:table) { TTY::Table.new rows } 12 | 13 | it 'displays table without styling' do 14 | expect(renderer.render).to eq unindent(<<-EOS) 15 | a1 a2 a3 16 | b1 b2 b3 17 | EOS 18 | end 19 | end 20 | 21 | context 'with header and rows' do 22 | it 'displays table with header' do 23 | expect(renderer.render).to eq unindent(<<-EOS) 24 | h1 h2 h3 25 | a1 a2 a3 26 | b1 b2 b3 27 | EOS 28 | end 29 | end 30 | 31 | context 'with short header' do 32 | let(:header) { ['h1', 'h2'] } 33 | let(:rows) { [['aaa1', 'a2'], ['b1', 'bb1']] } 34 | 35 | it 'displays table according to widths' do 36 | expect(renderer.render).to eq unindent(<<-EOS) 37 | h1 h2 38 | aaa1 a2 39 | b1 bb1 40 | EOS 41 | end 42 | end 43 | 44 | context 'with long header' do 45 | let(:header) { ['header1', 'header2', 'header3'] } 46 | 47 | it 'header greater than row sizes' do 48 | expect(renderer.render).to eq unindent(<<-EOS) 49 | header1 header2 header3 50 | a1 a2 a3 51 | b1 b2 b3 52 | EOS 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/unit/each_with_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '.each_with_index' do 4 | 5 | context 'with no block' do 6 | it 'returns enumerable' do 7 | table = TTY::Table.new ['h1','h2'], [['a1','a2'],['b1','b2']] 8 | expect(table.each_with_index).to be_instance_of(to_enum.class) 9 | end 10 | 11 | it 'yields the expected values' do 12 | table = TTY::Table.new ['h1','h2'], [['a1','a2'],['b1','b2']] 13 | expect(table.each_with_index.to_a).to eql(table.to_a) 14 | end 15 | end 16 | 17 | context 'with block' do 18 | context 'without header' do 19 | it "yields rows with expected data" do 20 | yields = [] 21 | table = TTY::Table.new [['a1','a2'],['b1','b2']] 22 | expected = [ 23 | [['a1','a2'], 0], 24 | [['b1','b2'], 1] 25 | ] 26 | expect { 27 | table.each_with_index { |row, indx| yields << [row, indx] } 28 | }.to change { yields }.from([]).to(expected) 29 | end 30 | end 31 | 32 | context 'with header' do 33 | it "yields header and rows with expected data" do 34 | yields = [] 35 | table = TTY::Table.new ['h1','h2'], [['a1','a2'],['b1','b2']] 36 | 37 | expected = [ 38 | [['h1','h2'], 0], 39 | [['a1','a2'], 1], 40 | [['b1','b2'], 2] 41 | ] 42 | 43 | expect { 44 | table.each_with_index { |row, indx| yields << [row, indx] } 45 | }.to change { yields }.from( [] ).to( expected ) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/unit/columns/extract_widths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Columns, '#extract_widths' do 4 | let(:color) { Pastel.new(enabled: true) } 5 | 6 | it 'extract widths' do 7 | header = ['h1', 'h2', 'h3'] 8 | rows = [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] 9 | table = TTY::Table.new(header, rows) 10 | expect(described_class.extract_widths(table.data)).to eql([2,2,2]) 11 | end 12 | 13 | it "extracts widths from utf" do 14 | header = ['h1', 'うなじ'] 15 | rows = [['こんにちは', 'a2'], ['b1','選択']] 16 | table = TTY::Table.new(header, rows) 17 | expect(described_class.extract_widths(table.data)).to eql([10,6]) 18 | end 19 | 20 | it "extracts widths from multiline text" do 21 | table = TTY::Table.new 22 | table << ["Multi\nLine\nContent", "Text\nthat\nwraps"] 23 | table << ["Some\nother\ntext", 'Simple'] 24 | expect(described_class.extract_widths(table.data)).to eq([7,6]) 25 | end 26 | 27 | it "extracts widths from multiline text" do 28 | table = TTY::Table.new 29 | table << ["Multi\\nLine\\nContent", "Text\\nthat\\nwraps"] 30 | table << ["Some\\nother\\ntext", 'Simple'] 31 | expect(described_class.extract_widths(table.data)).to eq([20, 17]) 32 | end 33 | 34 | it "extracts widths from ANSI text" do 35 | header = [color.green('h1'), 'h2'] 36 | table = TTY::Table.new header: header 37 | table << [color.green.on_blue('a1'), 'a2'] 38 | table << ['b1', color.red.on_yellow('b2')] 39 | expect(described_class.extract_widths(table.data)).to eq([2,2]) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/border/unicode/rendering_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Border::Unicode, '#rendering' do 4 | 5 | subject(:border) { described_class.new(column_widths) } 6 | 7 | context 'with empty row' do 8 | let(:row) { TTY::Table::Row.new([]) } 9 | let(:column_widths) { [] } 10 | 11 | it 'draws top line' do 12 | expect(border.top_line).to eq("┌┐") 13 | end 14 | 15 | it 'draws middle line' do 16 | expect(border.middle_line).to eq("├┤") 17 | end 18 | 19 | it 'draw bottom line' do 20 | expect(border.bottom_line).to eq("└┘") 21 | end 22 | 23 | it 'draws row line' do 24 | expect(border.row_line(row)).to eq("││") 25 | end 26 | end 27 | 28 | context 'with row' do 29 | let(:row) { TTY::Table::Row.new(['a1', 'a2', 'a3']) } 30 | let(:column_widths) { [2,2,2] } 31 | 32 | it 'draws top line' do 33 | expect(border.top_line).to eq("┌──┬──┬──┐") 34 | end 35 | 36 | it 'draw middle line' do 37 | expect(border.middle_line).to eq("├──┼──┼──┤") 38 | end 39 | 40 | it 'draw bottom line' do 41 | expect(border.bottom_line).to eq("└──┴──┴──┘") 42 | end 43 | 44 | it 'draws row line' do 45 | expect(border.row_line(row)).to eq("│a1│a2│a3│") 46 | end 47 | end 48 | 49 | context 'with multiline row' do 50 | let(:row) { TTY::Table::Row.new(["a1\nb1\nc1", 'a2', 'a3']) } 51 | let(:column_widths) { [2,2,2] } 52 | 53 | it 'draws row line' do 54 | expect(border.row_line(row)).to eq unindent(<<-EOS) 55 | │a1│a2│a3│ 56 | │b1│ │ │ 57 | │c1│ │ │ 58 | EOS 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/unit/field/equality_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Field, "equality" do 4 | let(:value) { "1" } 5 | let(:object) { described_class.new(value) } 6 | 7 | describe "#==" do 8 | it "is equal with the same object" do 9 | expect(object).to eq(object) 10 | end 11 | 12 | it "is equal with an quivalent object" do 13 | expect(object).to eq(object.dup) 14 | end 15 | 16 | it "is equal with an equivalent object of subclass" do 17 | other = Class.new(described_class).new(value) 18 | expect(object).to eq(other) 19 | end 20 | 21 | it "isn't equal with an object having a dirrent value" do 22 | other = described_class.new("2") 23 | expect(object).to_not eq(other) 24 | end 25 | end 26 | 27 | describe "#eql" do 28 | it "is equal with the same object" do 29 | expect(object).to eql(object) 30 | end 31 | 32 | it "is equal with an quivalent object" do 33 | expect(object).to eql(object.dup) 34 | end 35 | 36 | it "is equal with an equivalent object of subclass" do 37 | other = described_class.new(value) 38 | expect(object).to eql(other) 39 | end 40 | 41 | it "isn't equal with an object having a dirrent value" do 42 | other = described_class.new("2") 43 | expect(object).to_not eql(other) 44 | end 45 | end 46 | 47 | describe "#inspect" do 48 | it "displays object information" do 49 | expect(object.inspect).to eq("#") 51 | end 52 | end 53 | 54 | describe "#hash" do 55 | it "calculates object hash" do 56 | expect(object.hash).to be_a_kind_of(Numeric) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/tty/table/alignment_set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | class Table 5 | # A class responsible for column alignments 6 | # 7 | # Used internally by {TTY::Table::Operation::Alignment} 8 | class AlignmentSet 9 | include Enumerable 10 | 11 | DEFAULT = :left 12 | 13 | # Initialize an AlignmentSet 14 | # 15 | # @param [AlignmentSet, Array] alignments 16 | # the alignments for the renderer 17 | # 18 | # @api private 19 | def initialize(alignments) 20 | @alignments = Array(alignments).map(&:to_sym) 21 | end 22 | 23 | # Iterate over each element in the alignment set 24 | # 25 | # @example 26 | # alignment = AlignmentSet.new [1,2,3] 27 | # alignment.each { |element| ... } 28 | # 29 | # @return [self] 30 | # 31 | # @api public 32 | def each 33 | return to_enum unless block_given? 34 | to_ary.each { |element| yield element } 35 | self 36 | end 37 | 38 | # Lookup an alignment by index 39 | # 40 | # @param [Integer] index 41 | # 42 | # @return [Symbol] alignment 43 | # 44 | # @api public 45 | def [](index) 46 | alignments.fetch(index, DEFAULT) 47 | end 48 | 49 | # Convert to array 50 | # 51 | # @return [Array] 52 | # 53 | # @api public 54 | def to_ary 55 | alignments.to_a 56 | end 57 | 58 | # String representation of aligments 59 | # 60 | # @return [String] 61 | # 62 | # @api public 63 | def to_s 64 | to_ary 65 | end 66 | 67 | protected 68 | 69 | attr_reader :alignments 70 | end # AlignmentSet 71 | end # Table 72 | end # TTY 73 | -------------------------------------------------------------------------------- /lib/tty/table/operation/alignment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "strings" 4 | 5 | module TTY 6 | class Table 7 | module Operation 8 | # A class which responsiblity is to align table rows and header. 9 | class Alignment 10 | DEFAULT = :left 11 | 12 | # Initialize an Alignment operation 13 | # 14 | # @api private 15 | def initialize(alignments, widths = nil) 16 | @alignments = alignments 17 | @widths = widths 18 | end 19 | 20 | # Evaluate alignment of the provided row 21 | # 22 | # @param [TTY::Table::Field] field 23 | # the table field 24 | # 25 | # @param [Array] row 26 | # the table row 27 | # 28 | # @param [Integer] col 29 | # the table column index 30 | # 31 | # @return [TTY::Table::Field] 32 | # 33 | # @api public 34 | def call(field, row, col) 35 | align_field(field, col) 36 | end 37 | 38 | protected 39 | 40 | attr_reader :alignments 41 | 42 | attr_reader :widths 43 | 44 | # Align each field in a row 45 | # 46 | # @param [TTY::Table::Field] field 47 | # the table field 48 | # 49 | # @param [Integer] col 50 | # the table column index 51 | # 52 | # @return [TTY::Table::Field] 53 | # 54 | # @api private 55 | def align_field(field, col) 56 | column_width = widths[col] 57 | direction = field.alignment || alignments[col] || DEFAULT 58 | Strings.align(field.content, column_width, direction: direction) 59 | end 60 | end # Alignment 61 | end # Operation 62 | end # Table 63 | end # TTY 64 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/render_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, '#render' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new header, rows } 7 | 8 | subject(:renderer) { described_class.new(table) } 9 | 10 | context 'only rows' do 11 | let(:table) { TTY::Table.new rows } 12 | 13 | it 'display table rows' do 14 | expect(renderer.render).to eq unindent(<<-EOS) 15 | +--+--+--+ 16 | |a1|a2|a3| 17 | |b1|b2|b3| 18 | +--+--+--+ 19 | EOS 20 | end 21 | end 22 | 23 | context 'with header' do 24 | it 'displays table with header' do 25 | expect(renderer.render).to eq unindent(<<-EOS) 26 | +--+--+--+ 27 | |h1|h2|h3| 28 | +--+--+--+ 29 | |a1|a2|a3| 30 | |b1|b2|b3| 31 | +--+--+--+ 32 | EOS 33 | end 34 | end 35 | 36 | context 'with short header' do 37 | let(:header) { ['h1', 'h2'] } 38 | let(:rows) { [['aaa1', 'a2'], ['b1', 'bb1']] } 39 | 40 | it 'displays table according to widths' do 41 | expect(renderer.render).to eq unindent(<<-EOS) 42 | +----+---+ 43 | |h1 |h2 | 44 | +----+---+ 45 | |aaa1|a2 | 46 | |b1 |bb1| 47 | +----+---+ 48 | EOS 49 | end 50 | end 51 | 52 | context 'with long header' do 53 | let(:header) { ['header1', 'header2', 'header3'] } 54 | 55 | it 'header greater than row sizes' do 56 | expect(renderer.render).to eq unindent(<<-EOS) 57 | +-------+-------+-------+ 58 | |header1|header2|header3| 59 | +-------+-------+-------+ 60 | |a1 |a2 |a3 | 61 | |b1 |b2 |b3 | 62 | +-------+-------+-------+ 63 | EOS 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/unit/renderer/unicode/render_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Unicode, '#render' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new header, rows } 7 | 8 | subject(:renderer) { described_class.new(table) } 9 | 10 | context 'with rows only' do 11 | let(:table) { TTY::Table.new rows } 12 | 13 | it 'display table rows' do 14 | expect(renderer.render).to eq unindent(<<-EOS) 15 | ┌──┬──┬──┐ 16 | │a1│a2│a3│ 17 | │b1│b2│b3│ 18 | └──┴──┴──┘ 19 | EOS 20 | end 21 | end 22 | 23 | context 'with header' do 24 | it 'displays table with header' do 25 | expect(renderer.render).to eq unindent(<<-EOS) 26 | ┌──┬──┬──┐ 27 | │h1│h2│h3│ 28 | ├──┼──┼──┤ 29 | │a1│a2│a3│ 30 | │b1│b2│b3│ 31 | └──┴──┴──┘ 32 | EOS 33 | end 34 | end 35 | 36 | context 'with short header' do 37 | let(:header) { ['h1', 'h2'] } 38 | let(:rows) { [['aaa1', 'a2'], ['b1', 'bb1']] } 39 | 40 | it 'displays table according to widths' do 41 | expect(renderer.render).to eq unindent(<<-EOS) 42 | ┌────┬───┐ 43 | │h1 │h2 │ 44 | ├────┼───┤ 45 | │aaa1│a2 │ 46 | │b1 │bb1│ 47 | └────┴───┘ 48 | EOS 49 | end 50 | end 51 | 52 | context 'with long header' do 53 | let(:header) { ['header1', 'header2', 'header3'] } 54 | 55 | it 'header greater than row sizes' do 56 | expect(renderer.render).to eq unindent(<<-EOS) 57 | ┌───────┬───────┬───────┐ 58 | │header1│header2│header3│ 59 | ├───────┼───────┼───────┤ 60 | │a1 │a2 │a3 │ 61 | │b1 │b2 │b3 │ 62 | └───────┴───────┴───────┘ 63 | EOS 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/unit/row/equality_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Row, '#==' do 4 | let(:attributes) { [:id] } 5 | let(:data) { ['1'] } 6 | let(:object) { described_class.new(data, attributes) } 7 | 8 | subject { object == other } 9 | 10 | context 'with the same object' do 11 | let(:other) { object } 12 | 13 | it { is_expected.to eql(true) } 14 | 15 | it 'is symmetric' do 16 | is_expected.to eql(other == object) 17 | end 18 | end 19 | 20 | context 'with an equivalent object' do 21 | let(:other) { object.dup } 22 | 23 | it { is_expected.to eql(true) } 24 | 25 | it 'is symmetric' do 26 | is_expected.to eql(other == object) 27 | end 28 | end 29 | 30 | context 'with an equivalent object of subclass' do 31 | let(:other) { Class.new(described_class).new(data, attributes: attributes) } 32 | 33 | it { is_expected.to eql(true) } 34 | 35 | it 'is symmetric' do 36 | is_expected.to eql(other == object) 37 | end 38 | end 39 | 40 | context 'with an object having a different attributes' do 41 | let(:other_attributes) { [:text] } 42 | let(:other) { described_class.new(data, attributes: other_attributes) } 43 | 44 | it { is_expected.to eql(true) } 45 | 46 | it 'is symmetric' do 47 | is_expected.to eql(other == object) 48 | end 49 | end 50 | 51 | context 'with an object having a different attributes' do 52 | let(:other_data) { [2] } 53 | let(:other) { described_class.new(other_data, attributes: attributes) } 54 | 55 | it { is_expected.to eql(false) } 56 | 57 | it 'is symmetric' do 58 | is_expected.to eql(other == object) 59 | end 60 | end 61 | 62 | context 'with an equivalent object responding to_ary' do 63 | let(:other) { data } 64 | 65 | it { is_expected.to eql(true) } 66 | 67 | it 'is symmetric' do 68 | is_expected.to eql(other == object) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/unit/eql_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, "#eql?" do 4 | let(:header) { %w[h1 h2] } 5 | let(:rows) { [%w[a1 a2], %w[b1 b2]] } 6 | 7 | describe "#==" do 8 | it "is equivalent with the same table" do 9 | object = described_class.new(rows) 10 | expect(object).to eq(object) 11 | end 12 | 13 | it "is equivalent with a table containing same data" do 14 | expect(described_class.new(header, rows)). 15 | to eq(described_class.new(header, rows)) 16 | end 17 | 18 | it "is not equivalent with a table containing different data" do 19 | expect(described_class.new(%w[h1 h2], rows)). 20 | to_not eq(described_class.new(%w[h3 h4], rows)) 21 | end 22 | 23 | it "is not equivalent to another type" do 24 | expect(described_class.new(rows)).to_not eq(:other) 25 | end 26 | end 27 | 28 | describe "#eql?" do 29 | it "is equal with the same table object" do 30 | object = described_class.new(rows) 31 | expect(object).to eql(object) 32 | end 33 | 34 | it "is equal with a table containing same data" do 35 | expect(described_class.new(header, rows)). 36 | to eql(described_class.new(header, rows)) 37 | end 38 | 39 | it "is not equal with a table containing different data" do 40 | expect(described_class.new(header, rows)). 41 | to_not eql(described_class.new(%w[h3 h4], rows)) 42 | end 43 | 44 | it "is not equal to another type" do 45 | expect(described_class.new(rows)).to_not eql(:other) 46 | end 47 | end 48 | 49 | describe "#inspect" do 50 | it "displays object information" do 51 | expect(described_class.new(rows).inspect).to match(/# ['a1','a2'], 'h2' => ['b1','b2']}] 61 | expect(table.to_a).to eql([['h1','h2'], ['a1','a2'], ['b1','b2']]) 62 | end 63 | end 64 | 65 | context 'coercion' do 66 | it 'converts row arguments from hash to array' do 67 | table = TTY::Table.new rows: {a: 1, b: 2} 68 | expect(table.to_a).to eql([[:a,1],[:b,2]]) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/unit/border/ascii/rendering_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Border::ASCII, '#rendering' do 4 | 5 | subject(:border) { described_class.new(column_widths) } 6 | 7 | context 'with empty row' do 8 | let(:row) { TTY::Table::Row.new([]) } 9 | let(:column_widths) { [] } 10 | 11 | it 'draws top line' do 12 | expect(border.top_line).to eq("++") 13 | end 14 | 15 | it 'draws middle line' do 16 | expect(border.middle_line).to eq("++") 17 | end 18 | 19 | it 'draw bottom line' do 20 | expect(border.bottom_line).to eq("++") 21 | end 22 | 23 | it 'draws row line' do 24 | expect(border.row_line(row)).to eq("||") 25 | end 26 | end 27 | 28 | context 'with row' do 29 | let(:column_widths) { [2,2,2] } 30 | let(:row) { TTY::Table::Row.new(['a1', 'a2', 'a3']) } 31 | 32 | it 'draws top line' do 33 | expect(border.top_line).to eq("+--+--+--+") 34 | end 35 | 36 | it 'draw middle line' do 37 | expect(border.middle_line).to eq("+--+--+--+") 38 | end 39 | 40 | it 'draw bottom line' do 41 | expect(border.bottom_line).to eq("+--+--+--+") 42 | end 43 | 44 | it 'draws row line' do 45 | expect(border.row_line(row)).to eq("|a1|a2|a3|") 46 | end 47 | end 48 | 49 | context 'with multiline row' do 50 | let(:column_widths) { [2,2,2]} 51 | 52 | context 'with mixed data' do 53 | let(:row) { TTY::Table::Row.new(["a1\nb1\nc1", 'a2', 'a3']) } 54 | 55 | it 'draws row line' do 56 | expect(border.row_line(row)).to eq unindent(<<-EOS) 57 | |a1|a2|a3| 58 | |b1| | | 59 | |c1| | | 60 | EOS 61 | end 62 | end 63 | 64 | context 'with sparse data' do 65 | let(:row) { TTY::Table::Row.new(["a1\n\n", "\na2\n", "\n\na3"]) } 66 | 67 | it 'draws row line' do 68 | expect(border.row_line(row)).to eq unindent(<<-EOS) 69 | |a1| | | 70 | | |a2| | 71 | | | |a3| 72 | EOS 73 | end 74 | end 75 | 76 | context 'with empty data' do 77 | let(:row) { TTY::Table::Row.new(["\na1\n", "\na2\n", "\na3\n"]) } 78 | 79 | it 'draws row line' do 80 | expect(border.row_line(row)).to eq unindent(<<-EOS) 81 | | | | | 82 | |a1|a2|a3| 83 | | | | | 84 | EOS 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/unit/column_constraint/enforce_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::ColumnConstraint, '#enforce' do 4 | let(:header) { ['h1', 'h2', 'h3', 'h4'] } 5 | let(:rows) { [['a1', 'a2', 'a3', 'a4'], ['b1', 'b2', 'b3', 'b4']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:columns) { described_class.new(table, renderer) } 9 | 10 | context 'with width contraint' do 11 | let(:renderer) { TTY::Table::Renderer::Basic.new(table, options) } 12 | let(:options) { { width: 5 }} 13 | 14 | it 'raises error when table width is too small' do 15 | expect { 16 | columns.enforce 17 | }.to raise_error(TTY::Table::ResizeError) 18 | end 19 | end 20 | 21 | context 'with width contraint matching natural width' do 22 | let(:renderer) { TTY::Table::Renderer::Basic.new(table, options) } 23 | let(:options) { { width: 11, resize: true }} 24 | 25 | it 'raises error when table width is too small' do 26 | allow(columns).to receive(:expand_column_widths) 27 | columns.enforce 28 | expect(columns).to have_received(:expand_column_widths) 29 | end 30 | end 31 | 32 | context 'with table larger than allowed width' do 33 | let(:renderer) { TTY::Table::Renderer::Basic.new(table, options) } 34 | 35 | context 'with resize' do 36 | let(:options) { { width: 8, resize: true } } 37 | 38 | it 'calls shrink' do 39 | allow(columns).to receive(:shrink) 40 | columns.enforce 41 | expect(columns).to have_received(:shrink) 42 | end 43 | end 44 | 45 | context 'without resize' do 46 | let(:options) { { width: 8, resize: false }} 47 | 48 | it 'changes table orientation to vertical' do 49 | allow(Kernel).to receive(:warn) 50 | expect(renderer.column_widths).to eql([2,2,2,2]) 51 | expect(table.orientation.name).to eql(:horizontal) 52 | column_widths = columns.enforce 53 | expect(column_widths).to eq([2,2]) 54 | expect(table.orientation.name).to eql(:vertical) 55 | end 56 | end 57 | end 58 | 59 | context 'with table less than allowed width' do 60 | let(:renderer) { TTY::Table::Renderer::Basic.new(table, options) } 61 | let(:options) { { width: 15 }} 62 | 63 | it "doesn't change original widths" do 64 | allow(Kernel).to receive(:warn) 65 | expect(renderer.column_widths).to eq([2,2,2,2]) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/unit/renderer/unicode/padding_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Unicode, 'padding' do 4 | let(:header) { ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra'] } 5 | let(:rows) { [['id', 'int(11)', 'YES', 'nil', 'NULL', '']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'with left & right padding' do 11 | let(:options) { {padding: [0,1,0,1]} } 12 | 13 | it 'pads each field' do 14 | expect(renderer.render).to eql unindent(<<-EOS) 15 | ┌───────┬─────────┬──────┬─────┬─────────┬───────┐ 16 | │ Field │ Type │ Null │ Key │ Default │ Extra │ 17 | ├───────┼─────────┼──────┼─────┼─────────┼───────┤ 18 | │ id │ int(11) │ YES │ nil │ NULL │ │ 19 | └───────┴─────────┴──────┴─────┴─────────┴───────┘ 20 | EOS 21 | end 22 | end 23 | 24 | context 'with top & bottom padding' do 25 | let(:options) { {padding: [1,0,1,0], multiline: true} } 26 | 27 | it 'pads each field' do 28 | expect(renderer.render).to eql unindent(<<-EOS) 29 | ┌─────┬───────┬────┬───┬───────┬─────┐ 30 | │ │ │ │ │ │ │ 31 | │Field│Type │Null│Key│Default│Extra│ 32 | │ │ │ │ │ │ │ 33 | ├─────┼───────┼────┼───┼───────┼─────┤ 34 | │ │ │ │ │ │ │ 35 | │id │int(11)│YES │nil│NULL │ │ 36 | │ │ │ │ │ │ │ 37 | └─────┴───────┴────┴───┴───────┴─────┘ 38 | EOS 39 | end 40 | end 41 | 42 | context 'with full padding' do 43 | let(:options) { {padding: [1,1,1,1], multiline: true} } 44 | 45 | it 'pads each field' do 46 | expect(renderer.render).to eql unindent(<<-EOS) 47 | ┌───────┬─────────┬──────┬─────┬─────────┬───────┐ 48 | │ │ │ │ │ │ │ 49 | │ Field │ Type │ Null │ Key │ Default │ Extra │ 50 | │ │ │ │ │ │ │ 51 | ├───────┼─────────┼──────┼─────┼─────────┼───────┤ 52 | │ │ │ │ │ │ │ 53 | │ id │ int(11) │ YES │ nil │ NULL │ │ 54 | │ │ │ │ │ │ │ 55 | └───────┴─────────┴──────┴─────┴─────────┴───────┘ 56 | EOS 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/unit/rotate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#rotate' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | 7 | subject(:table) { described_class.new(header, rows) } 8 | 9 | before { table.orientation = :horizontal } 10 | 11 | context 'with default' do 12 | context 'without header' do 13 | let(:header) { nil } 14 | 15 | it 'preserves orientation' do 16 | expect(table.header).to be_nil 17 | expect(table.rotate.to_a).to eql rows 18 | end 19 | end 20 | 21 | context 'with header' do 22 | it 'preserves orientation' do 23 | expect(table.rotate.to_a).to eql [header] + rows 24 | end 25 | end 26 | end 27 | 28 | context 'with no header' do 29 | let(:header) { nil } 30 | 31 | it 'rotates the rows' do 32 | table.orientation = :vertical 33 | expect(table.rotate.to_a).to eql [ 34 | ['1', 'a1'], 35 | ['2', 'a2'], 36 | ['3', 'a3'], 37 | ['1', 'b1'], 38 | ['2', 'b2'], 39 | ['3', 'b3'], 40 | ] 41 | expect(table.header).to be_nil 42 | end 43 | 44 | it 'rotates the rows back' do 45 | table.orientation = :vertical 46 | table.rotate 47 | table.orientation = :horizontal 48 | expect(table.rotate.to_a).to eql rows 49 | expect(table.header).to eql header 50 | end 51 | 52 | it 'roates the output' do 53 | expect(table.to_s).to eq("a1 a2 a3\nb1 b2 b3") 54 | table.orientation = :vertical 55 | table.rotate 56 | expect(table.to_s).to eq("1 a1\n2 a2\n3 a3\n1 b1\n2 b2\n3 b3") 57 | end 58 | end 59 | 60 | context 'with header' do 61 | it 'rotates the rows and merges header' do 62 | table.orientation = :vertical 63 | expect(table.rotate.to_a).to eql [ 64 | ['h1', 'a1'], 65 | ['h2', 'a2'], 66 | ['h3', 'a3'], 67 | ['h1', 'b1'], 68 | ['h2', 'b2'], 69 | ['h3', 'b3'], 70 | ] 71 | expect(table.header).to be_empty 72 | end 73 | 74 | it 'rotates the rows and header back' do 75 | table.orientation = :vertical 76 | table.rotate 77 | expect(table.orientation).to be_a TTY::Table::Orientation::Vertical 78 | 79 | table.orientation = :horizontal 80 | expect(table.rotate.to_a).to eql [header] + rows 81 | expect(table.header).to eql header 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/multiline_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'multiline content' do 4 | context 'with escaping' do 5 | it "renders multiline as single line" do 6 | rows = [ ["First", '1'], ["Multiline\nContent", '2'], ["Third", '3']] 7 | table = TTY::Table.new rows 8 | renderer = described_class.new(table, multiline: false) 9 | expect(renderer.render).to eq unindent(<<-EOS) 10 | First 1 11 | Multiline\\nContent 2 12 | Third 3 13 | EOS 14 | end 15 | 16 | it "truncates multiline content" do 17 | rows = [ ["First", '1'], ["Multiline\nContent", '2'], ["Third", '3']] 18 | table = TTY::Table.new rows 19 | renderer = described_class.new(table, multiline: false, column_widths: [8,1]) 20 | expect(renderer.render).to eq unindent(<<-EOS) 21 | First 1 22 | Multil… 2 23 | Third 3 24 | EOS 25 | end 26 | end 27 | 28 | context 'without escaping' do 29 | it "renders every line" do 30 | rows = [["First", '1'], 31 | ["Multi\nLine\nContent", '2'], 32 | ["Third", "Multi\nLine"]] 33 | table = TTY::Table.new rows 34 | renderer = described_class.new(table, multiline: true) 35 | expect(renderer.render).to eq unindent(<<-EOS) 36 | First 1 37 | Multi 2 38 | Line 39 | Content 40 | Third Multi 41 | Line 42 | EOS 43 | end 44 | 45 | it "renders multiline with column widths" do 46 | rows = [["First", '1'], ["Multi\nLine\nContent", '2'], ["Third", '3']] 47 | table = TTY::Table.new rows 48 | renderer = described_class.new(table, multiline: true, column_widths: [8,1]) 49 | expect(renderer.render).to eq unindent(<<-EOS) 50 | First 1 51 | Multi 2 52 | Line 53 | Content 54 | Third 3 55 | EOS 56 | end 57 | 58 | it 'wraps multi line' do 59 | rows = [["First", '1'], ["Multi\nLine\nContent", '2'], ["Third", '3']] 60 | table = TTY::Table.new rows 61 | renderer = described_class.new(table, multiline: true, column_widths: [5,1]) 62 | expect(renderer.render).to eq unindent(<<-EOS) 63 | First 1 64 | Multi 2 65 | Line 66 | Conte 67 | nt 68 | Third 3 69 | EOS 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/unit/alignment_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'alignment' do 4 | it "aligns table columns when rendering" do 5 | table = TTY::Table.new ['header1', 'header2'], [['a1','a2'],['b1','b2']] 6 | expect(table.render(alignments: [:right, :center])).to eql([ 7 | "header1 header2", 8 | " a1 a2 ", 9 | " b1 b2 " 10 | ].join("\n")) 11 | end 12 | 13 | it "uses default alignment when too few alignments provided" do 14 | table = TTY::Table.new ['header1', 'header2'], [['a1','a2'],['b1','b2']] 15 | expect(table.render(alignments: [:right])).to eql([ 16 | "header1 header2", 17 | " a1 a2 ", 18 | " b1 b2 " 19 | ].join("\n")) 20 | end 21 | 22 | it "aligns table columns without header when rendering" do 23 | table = TTY::Table.new [['aaaaa1','a2'],['b1','bbbbb2']] 24 | expect(table.render(alignments: [:right, :center])).to eql([ 25 | "aaaaa1 a2 ", 26 | " b1 bbbbb2" 27 | ].join("\n")) 28 | end 29 | 30 | it "aligns individual fields when rendering" do 31 | table = TTY::Table.new header: ['header1', 'header2'] 32 | table << ['a1', {value: 'a2', alignment: :center}] 33 | table << [{value: 'b1', alignment: :right}, 'b2'] 34 | expect(table.render).to eql([ 35 | "header1 header2", 36 | "a1 a2 ", 37 | " b1 b2 " 38 | ].join("\n")) 39 | end 40 | 41 | it "prioritizes individual field options over table rendering options" do 42 | table = TTY::Table.new header: ['header1', 'header2'] 43 | table << [{value: 'a1', alignment: :center},'a2'] 44 | table << ['b1','b2'] 45 | expect(table.render(alignments: [:right, :center])).to eql([ 46 | "header1 header2", 47 | " a1 a2 ", 48 | " b1 b2 " 49 | ].join("\n")) 50 | end 51 | 52 | it "allows to align all columns at once" do 53 | table = TTY::Table.new ['header1', 'header2'], [['a1','a2'],['b1','b2']] 54 | expect(table.render(alignment: [:center])).to eql([ 55 | "header1 header2", 56 | " a1 a2 ", 57 | " b1 b2 " 58 | ].join("\n")) 59 | end 60 | 61 | xit "aligns specific column" do 62 | table = TTY::Table.new ['header1', 'header2'], [['a1','a2'],['b1','b2']] 63 | expect(table.render(column_alignment: [1, :center])).to eql([ 64 | "header1 header2", 65 | "a1 a2 ", 66 | "b1 b2 " 67 | ].join("\n")) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/tty/table/validatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "error" 4 | 5 | module TTY 6 | class Table 7 | # Mixin to provide validation for {Table}. 8 | # 9 | # Include this mixin to add validation for options. 10 | # 11 | # @api private 12 | module Validatable 13 | # Check if table rows are the equal size 14 | # 15 | # @raise [DimensionMismatchError] 16 | # if the rows are not equal length 17 | # 18 | # @return [nil] 19 | # 20 | # @api private 21 | def assert_row_sizes(rows) 22 | size = (rows[0] || []).size 23 | rows.each do |row| 24 | next if row.size == size 25 | raise TTY::Table::DimensionMismatchError, 26 | "row size differs (#{row.size} should be #{size})" 27 | end 28 | end 29 | 30 | # Check if table row is the correct size 31 | # 32 | # @raise [DimensionMismatchError] 33 | # if the row is not the correct length 34 | # 35 | # @return [nil] 36 | # 37 | # @api private 38 | def assert_row_size(row, rows) 39 | return if rows.empty? 40 | size = rows.last.size 41 | return if row.size == size 42 | raise TTY::Table::DimensionMismatchError, 43 | "row size differs (#{row.size} should be #{size})" 44 | end 45 | 46 | # Check if table type is provided 47 | # 48 | # @raise [ArgumentRequired] 49 | # 50 | # @return [Table] 51 | # 52 | # @api private 53 | def assert_table_type(value) 54 | return value if value.is_a?(TTY::Table) 55 | raise ArgumentRequired, 56 | "Expected TTY::Table instance, got #{value.inspect}" 57 | end 58 | 59 | # def assert_matching_widths(rows) 60 | # end 61 | # 62 | # def assert_string_values(rows) 63 | # end 64 | 65 | # Check if options are of required type 66 | # 67 | # @api private 68 | def validate_options!(options) 69 | header = options[:header] 70 | rows = options[:rows] 71 | 72 | if header && (!header.is_a?(Array) || header.empty?) 73 | raise InvalidArgument, ":header must be a non-empty array" 74 | end 75 | 76 | if rows && !(rows.is_a?(Array) || rows.is_a?(Hash)) 77 | raise InvalidArgument, ":rows must be a non-empty array or hash" 78 | end 79 | end 80 | end # Validatable 81 | end # Table 82 | end # TTY 83 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/padding_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'padding' do 4 | let(:header) { ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra'] } 5 | let(:rows) { [['id', 'int(11)', 'YES', 'nil', 'NULL', '']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'with left & right padding' do 11 | let(:options) { {padding: [0,1,0,1]} } 12 | 13 | it 'pads each field' do 14 | expect(renderer.render).to eql <<-EOS.chomp 15 | Field Type Null Key Default Extra 16 | id int(11) YES nil NULL 17 | EOS 18 | end 19 | end 20 | 21 | context 'with top & bottom padding' do 22 | let(:options) { {padding: [1,0,1,0]} } 23 | 24 | it 'pads each field' do 25 | expect(renderer.render).to eql <<-EOS.chomp 26 | 27 | Field Type Null Key Default Extra 28 | 29 | 30 | id int(11) YES nil NULL 31 | 32 | EOS 33 | end 34 | end 35 | 36 | context 'with full padding' do 37 | let(:options) { {padding: [1,1,1,1]} } 38 | 39 | it 'pads each field' do 40 | expect(renderer.render).to eql <<-EOS.chomp 41 | 42 | Field Type Null Key Default Extra 43 | 44 | 45 | id int(11) YES nil NULL 46 | 47 | EOS 48 | end 49 | end 50 | 51 | context "with multiline content padding" do 52 | it "pads around fields" do 53 | padding = [1,2,1,2] 54 | table = TTY::Table.new header: ['header1', 'header2'] 55 | table << ["a1\na1\na1",'a2'] 56 | table << ["b1","b2\nb2"] 57 | renderer = TTY::Table::Renderer::Basic.new(table, padding: padding, multiline: true) 58 | expect(renderer.render).to eql <<-EOS.chomp 59 | 60 | header1 header2 61 | 62 | 63 | a1 a2 64 | a1 65 | a1 66 | 67 | 68 | b1 b2 69 | b2 70 | 71 | EOS 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/single_row_separator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'individual row separators' do 4 | context 'using the separator option' do 5 | let(:header) { %w[h1 h2 h3] } 6 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3'], ['c1', 'c2', 'c3']] } 7 | let(:table) { TTY::Table.new(header, rows) } 8 | 9 | let(:object) { described_class.new table } 10 | 11 | subject(:renderer) { object } 12 | 13 | it "can use an array to specify the rows to separate" do 14 | renderer.border.separator = [2] 15 | expect(renderer.render).to eq unindent(<<-EOS) 16 | h1 h2 h3 17 | a1 a2 a3 18 | b1 b2 b3 19 | 20 | c1 c2 c3 21 | EOS 22 | end 23 | 24 | it "can use a proc to specify the rows to separate" do 25 | renderer.border.separator = ->(line) { line == 2 } 26 | expect(renderer.render).to eq unindent(<<-EOS) 27 | h1 h2 h3 28 | a1 a2 a3 29 | b1 b2 b3 30 | 31 | c1 c2 c3 32 | EOS 33 | end 34 | end 35 | 36 | it "works without a header row" do 37 | table = TTY::Table.new([%w[a1 a2 a3], %w[b1 b2 b3], %w[c1 c2 c3]]) 38 | renderer = described_class.new(table) 39 | renderer.border.separator = [1] 40 | expect(renderer.render).to eq unindent(<<-EOS) 41 | a1 a2 a3 42 | b1 b2 b3 43 | 44 | c1 c2 c3 45 | EOS 46 | end 47 | 48 | context 'using the :separator keyword as a row' do 49 | it "the :separator keyword can be used in the row definition" do 50 | table = TTY::Table.new([%w[a1 a2 a3], %w[b1 b2 b3], :separator, %w[c1 c2 c3]]) 51 | expect(described_class.new(table).render).to eq unindent(<<-EOS) 52 | a1 a2 a3 53 | b1 b2 b3 54 | 55 | c1 c2 c3 56 | EOS 57 | end 58 | 59 | it "the :separator keyword can be pushed into a run" do 60 | table = TTY::Table.new([['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']]) 61 | table << :separator << ['c1', 'c2', 'c3'] 62 | expect(described_class.new(table).render).to eq unindent(<<-EOS) 63 | a1 a2 a3 64 | b1 b2 b3 65 | 66 | c1 c2 c3 67 | EOS 68 | end 69 | 70 | it "the :separator keyword be used between the header and the body" do 71 | table = TTY::Table.new(['h1', 'h2', 'h3'], [:separator, ['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']]) 72 | expect(described_class.new(table).render).to eq unindent(<<-EOS) 73 | h1 h2 h3 74 | 75 | a1 a2 a3 76 | b1 b2 b3 77 | EOS 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/unit/renderer/unicode/coloring_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Unicode, 'coloring' do 4 | 5 | let(:color) { Pastel.new(enabled: true) } 6 | 7 | before { allow(Pastel).to receive(:new).and_return(color) } 8 | 9 | context 'when border' do 10 | it "colors border" do 11 | table = TTY::Table.new header: ['header1', 'header2'] 12 | table << ['a1', 'a2'] 13 | table << ['b1', 'b2'] 14 | renderer = described_class.new(table) 15 | renderer.border = {style: :green } 16 | 17 | expect(renderer.render).to eq unindent(<<-EOS) 18 | #{color.green('┌───────┬───────┐')} 19 | #{color.green('│')}header1#{color.green('│')}header2#{color.green('│')} 20 | #{color.green('├───────┼───────┤')} 21 | #{color.green('│')}a1 #{color.green('│')}a2 #{color.green('│')} 22 | #{color.green('│')}b1 #{color.green('│')}b2 #{color.green('│')} 23 | #{color.green('└───────┴───────┘')} 24 | EOS 25 | end 26 | end 27 | 28 | context 'when content' do 29 | it "colors individual field" do 30 | header = [color.yellow('header1'), 'header2'] 31 | table = TTY::Table.new header: header 32 | table << [color.green.on_blue('a1'), 'a2'] 33 | table << ['b1', color.red.on_yellow('b2')] 34 | renderer = described_class.new(table) 35 | 36 | expect(renderer.render).to eq unindent(<<-EOS) 37 | ┌───────┬───────┐ 38 | │#{color.yellow('header1')}│header2│ 39 | ├───────┼───────┤ 40 | │#{color.green.on_blue('a1')} │a2 │ 41 | │b1 │#{color.red.on_yellow('b2')} │ 42 | └───────┴───────┘ 43 | EOS 44 | end 45 | 46 | it "colors multiline content" do 47 | header = [color.yellow("Multi\nHeader"), 'header2'] 48 | table = TTY::Table.new header: header 49 | table << [color.green.on_blue("Multi\nLine\nContent"), 'a2'] 50 | table << ['b1', color.red.on_yellow("Multi\nLine\nContent")] 51 | renderer = described_class.new(table, multiline: true) 52 | 53 | expect(renderer.render).to eq unindent(<<-EOS) 54 | ┌───────┬───────┐ 55 | │#{color.yellow('Multi ')}│header2│ 56 | │#{color.yellow('Header')} │ │ 57 | ├───────┼───────┤ 58 | │#{color.green.on_blue('Multi ')}│a2 │ 59 | │#{color.green.on_blue('Line ')}│ │ 60 | │#{color.green.on_blue('Content')}│ │ 61 | │b1 │#{color.red.on_yellow("Multi ")}│ 62 | │ │#{color.red.on_yellow("Line ")}│ 63 | │ │#{color.red.on_yellow("Content")}│ 64 | └───────┴───────┘ 65 | EOS 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/alignment_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'alignment' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:options) { { alignments: alignments } } 7 | let(:table) { TTY::Table.new(header, rows) } 8 | 9 | subject(:renderer) { described_class.new table, options } 10 | 11 | context 'with default' do 12 | let(:header) { ['h1', 'h2'] } 13 | let(:rows) { [['aaaaa', 'a'], ['b', 'bbbbb']] } 14 | let(:alignments) { nil } 15 | 16 | it 'aligns left by default' do 17 | expect(renderer.render).to eql unindent(<<-EOS) 18 | h1 h2 19 | aaaaa a 20 | b bbbbb 21 | EOS 22 | end 23 | end 24 | 25 | context 'with different headers' do 26 | let(:header) { ['header1', 'head2', 'h3'] } 27 | let(:alignments) { [:left, :center, :right] } 28 | 29 | it 'aligns headers' do 30 | expect(renderer.render).to eql unindent(<<-EOS) 31 | header1 head2 h3 32 | a1 a2 a3 33 | b1 b2 b3 34 | EOS 35 | end 36 | end 37 | 38 | context 'with different aligns' do 39 | let(:header) { nil } 40 | let(:rows) { [['aaaaa', 'a'], ['b', 'bbbbb']] } 41 | let(:alignments) { [:left, :right] } 42 | 43 | it 'aligns table rows' do 44 | expect(renderer.render.to_s).to eql unindent(<<-EOS) 45 | aaaaa a 46 | b bbbbb 47 | EOS 48 | end 49 | end 50 | 51 | context 'with individual field aligns' do 52 | let(:header) { ['header1', 'header2', 'header3'] } 53 | let(:alignments) { [:left, :center, :right] } 54 | let(:options) { {alignments: alignments} } 55 | let(:table) { 56 | TTY::Table.new header: header do |t| 57 | t << ['a1', 'a2', 'a3'] 58 | t << ['b1', {value: 'b2', alignment: :right}, 'b3'] 59 | t << ['c1', 'c2', {value: 'c3', alignment: :center}] 60 | end 61 | } 62 | 63 | it "takes individual fields over global aligns" do 64 | expect(renderer.render).to eq unindent(<<-EOS) 65 | header1 header2 header3 66 | a1 a2 a3 67 | b1 b2 b3 68 | c1 c2 c3 69 | EOS 70 | end 71 | end 72 | 73 | context 'with aligned header' do 74 | let(:rows) { [['aaaaa1', 'a2', 'aaa3'], ['b1', 'bbbb2', 'bb3']] } 75 | let(:header) {['h1', {value: 'h2', alignment: :right}, {value: 'h3', alignment: :center}] } 76 | let(:options) { { renderer: :basic } } 77 | 78 | it "aligns headres" do 79 | expect(renderer.render).to eq unindent(<<-EOS) 80 | h1 h2 h3 81 | aaaaa1 a2 aaa3 82 | b1 bbbb2 bb3 83 | EOS 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/unit/renderer/border_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer, '#border' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:border) { nil } 7 | 8 | let(:table) { TTY::Table.new(header, rows) } 9 | 10 | subject(:renderer) { described_class.select(type).new(table) } 11 | 12 | context 'when basic renderer' do 13 | let(:type) { :basic } 14 | let(:border) { {characters: {'top' => '-'}, style: :red} } 15 | 16 | it 'specifies border in hash' do 17 | renderer.border(border) 18 | expect(renderer.border.characters['top']).to eql('-') 19 | end 20 | 21 | it 'specifies border in characters attribute' do 22 | renderer.border.characters = {'top' => '*'} 23 | expect(renderer.border.characters['top']).to eql('*') 24 | end 25 | 26 | it 'specifies border in block' do 27 | renderer.border do 28 | mid '=' 29 | mid_mid ' ' 30 | end 31 | 32 | expect(renderer.render).to eq unindent(<<-EOS) 33 | h1 h2 h3 34 | == == == 35 | a1 a2 a3 36 | b1 b2 b3 37 | EOS 38 | end 39 | end 40 | 41 | context 'when ascii renderer' do 42 | let(:type) { :ascii } 43 | 44 | it 'specifies border in block' do 45 | renderer.border do 46 | mid '=' 47 | mid_mid '=' 48 | mid_left '=' 49 | mid_right '=' 50 | end 51 | 52 | expect(renderer.render).to eq unindent(<<-EOS) 53 | +--+--+--+ 54 | |h1|h2|h3| 55 | ========== 56 | |a1|a2|a3| 57 | |b1|b2|b3| 58 | +--+--+--+ 59 | EOS 60 | end 61 | 62 | it 'specifies border as hash' do 63 | renderer.border({characters: { 64 | 'mid' => '=', 65 | 'mid_mid' => '=', 66 | 'mid_left' => '=', 67 | 'mid_right' => '=', 68 | }}) 69 | 70 | expect(renderer.render).to eq unindent(<<-EOS) 71 | +--+--+--+ 72 | |h1|h2|h3| 73 | ========== 74 | |a1|a2|a3| 75 | |b1|b2|b3| 76 | +--+--+--+ 77 | EOS 78 | end 79 | end 80 | 81 | context 'when unicode renderer' do 82 | let(:type) { :unicode } 83 | 84 | it 'specifies border in block' do 85 | renderer.border do 86 | mid '=' 87 | mid_mid '=' 88 | mid_left '=' 89 | mid_right '=' 90 | end 91 | 92 | expect(renderer.render).to eq unindent(<<-EOS) 93 | ┌──┬──┬──┐ 94 | │h1│h2│h3│ 95 | ========== 96 | │a1│a2│a3│ 97 | │b1│b2│b3│ 98 | └──┴──┴──┘ 99 | EOS 100 | end 101 | end 102 | end # border 103 | -------------------------------------------------------------------------------- /spec/unit/renderer/basic/resizing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::Basic, 'resizing' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'when expanding' do 11 | context 'even columns' do 12 | let(:options) { {width: 16, resize: true} } 13 | 14 | it 'resizes each column' do 15 | expect(renderer.render).to eql unindent(<<-EOS) 16 | h1 h2 h3 17 | a1 a2 a3 18 | b1 b2 b3 19 | EOS 20 | end 21 | end 22 | 23 | context 'even columns with extra width' do 24 | let(:header) { ['h1', 'h2', 'h3', 'h4'] } 25 | let(:rows) { [['a1','a2','a3','a4'], ['b1','b2','b3','b4']] } 26 | let(:options) { {width: 21, resize: true} } 27 | 28 | it 'resizes each column' do 29 | expect(renderer.render).to eql unindent(<<-EOS) 30 | h1 h2 h3 h4 31 | a1 a2 a3 a4 32 | b1 b2 b3 b4 33 | EOS 34 | end 35 | end 36 | 37 | context 'uneven columns' do 38 | let(:header) { ['h1', 'h2', 'h3'] } 39 | let(:rows) { [['aaa1', 'aa2', 'aaaaaaa3'], ['b1', 'b2', 'b3']] } 40 | let(:options) { {width: 32, resize: true} } 41 | 42 | it 'resizes each column' do 43 | expect(renderer.render).to eql unindent(<<-EOS) 44 | h1 h2 h3 45 | aaa1 aa2 aaaaaaa3 46 | b1 b2 b3 47 | EOS 48 | end 49 | end 50 | end 51 | 52 | context 'when shrinking' do 53 | let(:header) { ['head1', 'head2'] } 54 | let(:rows) { [['aaaa1','aaaa2',], ['bbbb1','bbbb2']] } 55 | 56 | context 'even columns' do 57 | let(:options) { {width: 8, resize: true} } 58 | 59 | it 'resizes each column' do 60 | expect(renderer.render).to eql unindent(<<-EOS) 61 | he… h… 62 | aa… a… 63 | bb… b… 64 | EOS 65 | end 66 | end 67 | 68 | context 'even columns with extra width' do 69 | let(:options) { {width: 9, resize: true} } 70 | 71 | it 'resizes each column' do 72 | expect(renderer.render).to eql unindent(<<-EOS) 73 | he… he… 74 | aa… aa… 75 | bb… bb… 76 | EOS 77 | end 78 | end 79 | 80 | context 'uneven columns' do 81 | let(:header) { ['head1', 'head2', 'head3'] } 82 | let(:rows) { [['aaa1', 'aa2', 'aaaaaaa3'], ['b1', 'b2', 'b3']] } 83 | let(:options) { {width: 16, resize: true} } 84 | 85 | it 'resizes each column' do 86 | expect(renderer.render).to eql unindent(<<-EOS) 87 | head1 he… head3 88 | aaa1 aa2 aaa… 89 | b1 b2 b3 90 | EOS 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/unit/render_with_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, '#render_with' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { described_class.new header, rows } 7 | let(:color) { Pastel.new(enabled: true) } 8 | 9 | before { allow(Pastel).to receive(:new).and_return(color) } 10 | 11 | context 'with invalid border class' do 12 | it "doesn't inherit from TTY::Table::Border" do 13 | expect { table.render_with String }.to raise_error(TTY::Table::TypeError) 14 | end 15 | 16 | it "doesn't implement def_border" do 17 | klass = Class.new(TTY::Table::Border) 18 | expect { table.render_with klass }. 19 | to raise_error(TTY::Table::NoImplementationError) 20 | end 21 | end 22 | 23 | context 'with complete border' do 24 | before { 25 | class MyBorder < TTY::Table::Border 26 | def_border do 27 | top '=' 28 | top_mid '*' 29 | top_left '*' 30 | top_right '*' 31 | bottom '=' 32 | bottom_mid '*' 33 | bottom_left '*' 34 | bottom_right '*' 35 | mid '=' 36 | mid_mid '*' 37 | mid_left '*' 38 | mid_right '*' 39 | left '$' 40 | center '$' 41 | right '$' 42 | end 43 | end 44 | } 45 | 46 | it 'displays custom border' do 47 | expect(table.render_with(MyBorder)).to eq unindent(<<-EOS) 48 | *==*==*==* 49 | $h1$h2$h3$ 50 | *==*==*==* 51 | $a1$a2$a3$ 52 | $b1$b2$b3$ 53 | *==*==*==* 54 | EOS 55 | end 56 | end 57 | 58 | context 'with incomplete border' do 59 | before { 60 | class MyBorder < TTY::Table::Border 61 | def_border do 62 | bottom ' ' 63 | bottom_mid '*' 64 | bottom_left '*' 65 | bottom_right '*' 66 | left '$' 67 | center '$' 68 | right '$' 69 | end 70 | end 71 | } 72 | 73 | it 'displays border' do 74 | expect(table.render_with(MyBorder)).to eq unindent(<<-EOS) 75 | $h1$h2$h3$ 76 | $a1$a2$a3$ 77 | $b1$b2$b3$ 78 | * * * * 79 | EOS 80 | end 81 | end 82 | 83 | context 'with renderer' do 84 | before { 85 | class MyBorder < TTY::Table::Border 86 | def_border do 87 | left '|' 88 | right '|' 89 | end 90 | end 91 | } 92 | 93 | it 'displays border' do 94 | result = table.render_with MyBorder do |renderer| 95 | renderer.border.style = :red 96 | end 97 | expect(result).to eq unindent(<<-EOS) 98 | \e[31m|\e[0mh1h2h3\e[31m|\e[0m 99 | \e[31m|\e[0ma1a2a3\e[31m|\e[0m 100 | \e[31m|\e[0mb1b2b3\e[31m|\e[0m 101 | EOS 102 | end 103 | end 104 | end # render_with 105 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/coloring_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, 'coloring' do 4 | 5 | let(:color) { Pastel.new(enabled: true) } 6 | 7 | before { allow(Pastel).to receive(:new).and_return(color) } 8 | 9 | context 'when border' do 10 | it "colors border" do 11 | table = TTY::Table.new header: ['header1', 'header2'] 12 | table << ['a1', 'a2'] 13 | table << ['b1', 'b2'] 14 | renderer = described_class.new(table) 15 | renderer.border.style = :green 16 | 17 | expect(renderer.render).to eq unindent(<<-EOS) 18 | #{color.green('+-------+-------+')} 19 | #{color.green('|')}header1#{color.green('|')}header2#{color.green('|')} 20 | #{color.green('+-------+-------+')} 21 | #{color.green('|')}a1 #{color.green('|')}a2 #{color.green('|')} 22 | #{color.green('|')}b1 #{color.green('|')}b2 #{color.green('|')} 23 | #{color.green('+-------+-------+')} 24 | EOS 25 | end 26 | end 27 | 28 | context 'when content' do 29 | it "colors individual field" do 30 | header = [color.yellow('header1'), 'header2'] 31 | table = TTY::Table.new header: header 32 | table << [color.green.on_blue('a1'), 'a2'] 33 | table << ['b1', color.red.on_yellow('b2')] 34 | renderer = described_class.new(table) 35 | 36 | expect(renderer.render).to eq unindent(<<-EOS) 37 | +-------+-------+ 38 | |#{color.yellow('header1')}|header2| 39 | +-------+-------+ 40 | |#{color.green.on_blue('a1')} |a2 | 41 | |b1 |#{color.red.on_yellow('b2')} | 42 | +-------+-------+ 43 | EOS 44 | end 45 | 46 | it "colors multiline content" do 47 | header = [color.yellow("Multi\nHeader"), 'header2'] 48 | table = TTY::Table.new header: header 49 | table << [color.green.on_blue("Multi\nLine\nContent"), 'a2'] 50 | table << ['b1', color.red.on_yellow("Multi\nLine\nContent")] 51 | renderer = described_class.new(table, multiline: true) 52 | 53 | expect(renderer.render).to eq unindent(<<-EOS) 54 | +-------+-------+ 55 | |#{color.yellow('Multi ')}|header2| 56 | |#{color.yellow('Header')} | | 57 | +-------+-------+ 58 | |#{color.green.on_blue('Multi ')}|a2 | 59 | |#{color.green.on_blue('Line ')}| | 60 | |#{color.green.on_blue('Content')}| | 61 | |b1 |#{color.red.on_yellow("Multi ")}| 62 | | |#{color.red.on_yellow("Line ")}| 63 | | |#{color.red.on_yellow("Content")}| 64 | +-------+-------+ 65 | EOS 66 | end 67 | 68 | xit "colors adjecent content" do 69 | hello = color.decorate("hello", :black, :on_green) 70 | world = color.decorate("world", :black, :on_red) 71 | 72 | table = TTY::Table.new [:header], [ 73 | [hello], 74 | [world], 75 | [hello + world], 76 | [hello + ' ' + world], 77 | ] 78 | renderer = described_class.new(table, column_widths: 6, multiline: true) 79 | 80 | rendered = renderer.render 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/tty/table/columns.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "error" 4 | 5 | module TTY 6 | class Table 7 | # A module for calculating table data column widths 8 | # 9 | # Used by {Table} to manage column sizing. 10 | # 11 | # @api private 12 | module Columns 13 | # Calculate total table width 14 | # 15 | # @return [Integer] 16 | # 17 | # @api public 18 | def total_width(data) 19 | extract_widths(data).reduce(:+) 20 | end 21 | module_function :total_width 22 | 23 | # Calcualte maximum column widths 24 | # 25 | # @return [Array] column widths 26 | # 27 | # @api private 28 | def extract_widths(data) 29 | colcount = data.max { |row_a, row_b| row_a.size <=> row_b.size }.size 30 | (0...colcount).reduce([]) do |maximas, col_index| 31 | maximas << find_maximum(data, col_index) 32 | maximas 33 | end 34 | end 35 | module_function :extract_widths 36 | 37 | # Find a maximum column width. The calculation takes into account 38 | # wether the content is escaped or not. 39 | # 40 | # @param [Integer] index 41 | # the column index 42 | # 43 | # @return [Integer] 44 | # 45 | # @api private 46 | def find_maximum(data, index) 47 | data.map do |row| 48 | (field = row.call(index)) ? field.length : 0 49 | end.max 50 | end 51 | module_function :find_maximum 52 | 53 | # Converts column widths to array format or infers default widths 54 | # 55 | # @param [TTY::Table] table 56 | # 57 | # @param [Array, Numeric, NilClass] column_widths 58 | # 59 | # @return [Array[Integer]] 60 | # 61 | # @api public 62 | def widths_from(table, column_widths = nil) 63 | case column_widths 64 | when Array 65 | assert_widths(column_widths, table.columns_size) 66 | Array(column_widths).map(&:to_i) 67 | when Numeric 68 | Array.new(table.columns_size, column_widths) 69 | when NilClass 70 | extract_widths(table.data) 71 | else 72 | raise TypeError, "Invalid type for column widths" 73 | end 74 | end 75 | module_function :widths_from 76 | 77 | # Assert data integrity for column widths 78 | # 79 | # @param [Array] column_widths 80 | # 81 | # @param [Integer] table_widths 82 | # 83 | # @raise [TTY::InvalidArgument] 84 | # 85 | # @api public 86 | def assert_widths(column_widths, table_widths) 87 | if column_widths.empty? 88 | raise InvalidArgument, "Value for :column_widths must be " \ 89 | "a non-empty array" 90 | end 91 | if column_widths.size != table_widths 92 | raise InvalidArgument, "Value for :column_widths must match " \ 93 | "table column count" 94 | end 95 | end 96 | module_function :assert_widths 97 | end # Columns 98 | end # Table 99 | end # TTY 100 | -------------------------------------------------------------------------------- /lib/tty/table/renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "error" 4 | require_relative "renderer/basic" 5 | require_relative "renderer/ascii" 6 | require_relative "renderer/unicode" 7 | 8 | module TTY 9 | class Table 10 | # A module responsible for selecting tabule data renderer 11 | # 12 | # Used internally by {Table} to render table content out. 13 | # 14 | # @api private 15 | module Renderer 16 | RENDERER_MAPPER = { 17 | ascii: TTY::Table::Renderer::ASCII, 18 | basic: TTY::Table::Renderer::Basic, 19 | unicode: TTY::Table::Renderer::Unicode 20 | } 21 | 22 | # Select renderer class based on string name. 23 | # 24 | # The possible values for type are 25 | # [:basic, :ascii, :unicode] 26 | # 27 | # @param [Symbol] type 28 | # the renderer type used for displaying table 29 | # 30 | # @return [TTY::Table::Renderer] 31 | # 32 | # @api private 33 | def select(type) 34 | RENDERER_MAPPER[type || :basic] 35 | end 36 | module_function :select 37 | 38 | # Raises an error if provided border class is of wrong type or has invalid 39 | # implementation 40 | # 41 | # @raise [TypeError] 42 | # raised when providing wrong class for border 43 | # 44 | # @raise [NoImplementationError] 45 | # raised when border class does not implement core methods 46 | # 47 | # @api public 48 | def assert_border_class(border_class) 49 | return unless border_class 50 | unless border_class <= TTY::Table::Border 51 | raise TypeError, 52 | "#{border_class} should inherit from TTY::Table::Border" 53 | end 54 | unless border_class.characters 55 | raise NoImplementationError, 56 | "#{border_class} should implement def_border" 57 | end 58 | end 59 | module_function :assert_border_class 60 | 61 | # Render a given table and return the string representation. 62 | # 63 | # @param [TTY::Table] table 64 | # the table to be rendered 65 | # 66 | # @param [Hash] options 67 | # the options to render the table with 68 | # @option options [String] :renderer 69 | # used to format table output 70 | # 71 | # @return [String] 72 | # 73 | # @api public 74 | def render(table, options = {}, &block) 75 | renderer = select(options[:renderer]).new(table, options) 76 | yield renderer if block_given? 77 | renderer.render 78 | end 79 | module_function :render 80 | 81 | # Add custom border for the renderer 82 | # 83 | # @param [TTY::Table::Border] border_class 84 | # 85 | # @param [TTY::Table] table 86 | # 87 | # @param [Hash] options 88 | # 89 | # @raise [TypeError] 90 | # raised if the klass does not inherit from Table::Border 91 | # 92 | # @raise [NoImplemntationError] 93 | # raise if the klass does not implement def_border 94 | # 95 | # @api public 96 | def render_with(border_class, table, options = {}, &block) 97 | assert_border_class(border_class) 98 | options[:border_class] = border_class if border_class 99 | render(table, options, &block) 100 | end 101 | module_function :render_with 102 | end # Renderer 103 | end # Table 104 | end # TTY 105 | -------------------------------------------------------------------------------- /spec/unit/border/null/rendering_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Border::Null, '#rendering' do 4 | let(:border) { nil } 5 | 6 | subject { described_class.new column_widths, border } 7 | 8 | context 'with empty row' do 9 | let(:row) { TTY::Table::Row.new([]) } 10 | let(:column_widths) { [] } 11 | 12 | it 'draws top line' do 13 | expect(subject.top_line).to be_nil 14 | end 15 | 16 | it 'draws middle line' do 17 | expect(subject.middle_line).to be_nil 18 | end 19 | 20 | it 'draw bottom line' do 21 | expect(subject.bottom_line).to be_nil 22 | end 23 | 24 | it 'draws row line' do 25 | expect(subject.row_line(row)).to eq('') 26 | end 27 | end 28 | 29 | context 'with row' do 30 | let(:row) { TTY::Table::Row.new(['a1', 'a2', 'a3']) } 31 | let(:column_widths) { [2,2,2] } 32 | 33 | it 'draws top line' do 34 | expect(subject.top_line).to be_nil 35 | end 36 | 37 | it 'draw middle line' do 38 | expect(subject.middle_line).to be_nil 39 | end 40 | 41 | it 'draw bottom line' do 42 | expect(subject.bottom_line).to be_nil 43 | end 44 | 45 | it 'draws row line' do 46 | expect(subject.row_line(row)).to eq('a1 a2 a3') 47 | end 48 | end 49 | 50 | context 'with multiline row' do 51 | let(:column_widths) { [2,2,2] } 52 | 53 | context 'with mixed data' do 54 | let(:row) { TTY::Table::Row.new(["a1\nb1\nc1", 'a2', 'a3']) } 55 | 56 | it 'draws row line' do 57 | expect(subject.row_line(row)).to eq unindent(<<-EOS) 58 | a1 a2 a3 59 | b1 60 | c1 61 | EOS 62 | end 63 | end 64 | 65 | context 'with sparse data' do 66 | let(:row) { TTY::Table::Row.new(["a1\n\n", "\na2\n", "\n\na3"]) } 67 | 68 | it 'draws row line' do 69 | expect(subject.row_line(row)).to eq <<-EOS.chomp 70 | a1 71 | a2 72 | a3 73 | EOS 74 | end 75 | end 76 | 77 | context 'with empty data' do 78 | let(:row) { TTY::Table::Row.new(["\na1\n", "\na2\n", "\na3\n"]) } 79 | 80 | it 'draws row line' do 81 | expect(subject.row_line(row)).to eq <<-EOS.chomp 82 | 83 | a1 a2 a3 84 | 85 | EOS 86 | end 87 | end 88 | end 89 | 90 | context 'with border' do 91 | let(:row) { TTY::Table::Row.new(['a1', 'a2', 'a3']) } 92 | let(:column_widths) { [2,2,2] } 93 | let(:border) { { :characters => { 94 | 'top' => '=', 95 | 'top_mid' => '=', 96 | 'top_left' => '=', 97 | 'top_right' => '=', 98 | 'bottom' => '=', 99 | 'bottom_mid' => '=', 100 | 'bottom_left' => '=', 101 | 'bottom_right' => '=', 102 | 'mid' => '=', 103 | 'mid_mid' => '=', 104 | 'mid_left' => '=', 105 | 'mid_right' => '=', 106 | 'left' => '=', 107 | 'center' => '=', 108 | 'right' => '=' 109 | } } } 110 | 111 | 112 | it 'draws top line' do 113 | expect(subject.top_line).to eql '==========' 114 | end 115 | 116 | it 'draws middle line line' do 117 | expect(subject.middle_line).to eql '==========' 118 | end 119 | 120 | it 'draws bottom line' do 121 | expect(subject.bottom_line).to eql '==========' 122 | end 123 | 124 | it 'draws row line' do 125 | expect(subject.row_line(row)).to eql '=a1=a2=a3=' 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/multiline_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, 'multiline content' do 4 | context 'with escaping' do 5 | it "renders multiline as single line" do 6 | rows = [["First", '1'], ["Multiline\nContent", '2'], ["Third", '3']] 7 | table = TTY::Table.new rows 8 | renderer = described_class.new(table, multiline: false) 9 | expect(renderer.render).to eq unindent(<<-EOS) 10 | +------------------+-+ 11 | |First |1| 12 | |Multiline\\nContent|2| 13 | |Third |3| 14 | +------------------+-+ 15 | EOS 16 | end 17 | 18 | it "truncates multiline content" do 19 | rows = [["First", '1'], ["Multiline\nContent", '2'], ["Third", '3']] 20 | table = TTY::Table.new rows 21 | renderer = described_class.new(table, multiline: false, column_widths: [8,1]) 22 | expect(renderer.render).to eq unindent(<<-EOS) 23 | +--------+-+ 24 | |First |1| 25 | |Multil… |2| 26 | |Third |3| 27 | +--------+-+ 28 | EOS 29 | end 30 | 31 | it "renders correctly multiline header as single line" do 32 | header = ["Multi\nHeader", "header2"] 33 | rows = [["First", '1'], ["Multiline\nContent", '2'], ["Third", '3']] 34 | table = TTY::Table.new header, rows 35 | renderer = described_class.new(table, multiline: false) 36 | expect(renderer.render).to eq unindent(<<-EOS) 37 | +------------------+-------+ 38 | |Multi\\nHeader |header2| 39 | +------------------+-------+ 40 | |First |1 | 41 | |Multiline\\nContent|2 | 42 | |Third |3 | 43 | +------------------+-------+ 44 | EOS 45 | end 46 | end 47 | 48 | context 'without escaping' do 49 | it "renders multiline" do 50 | rows = [["First", '1'], ["Multi\nLine\nContent", '2'], ["Third", '3']] 51 | table = TTY::Table.new rows 52 | renderer = described_class.new(table, multiline: true) 53 | expect(renderer.render).to eq unindent(<<-EOS) 54 | +-------+-+ 55 | |First |1| 56 | |Multi |2| 57 | |Line | | 58 | |Content| | 59 | |Third |3| 60 | +-------+-+ 61 | EOS 62 | end 63 | 64 | it "wraps multiline" do 65 | rows = [["First", '1'], ["Multi\nLine\nContent", '2'], ["Third", '3']] 66 | table = TTY::Table.new rows 67 | renderer = described_class.new(table, multiline: true, column_widths: [5,1]) 68 | expect(renderer.render).to eq unindent(<<-EOS) 69 | +-----+-+ 70 | |First|1| 71 | |Multi|2| 72 | |Line | | 73 | |Conte| | 74 | |nt | | 75 | |Third|3| 76 | +-----+-+ 77 | EOS 78 | end 79 | 80 | it "renders multilne header" do 81 | header = ["Multi\nHeader", "header2"] 82 | rows = [["First", '1'], ["Multi\nLine\nContent", '2'], ["Third", '3']] 83 | table = TTY::Table.new header, rows 84 | renderer = described_class.new(table, multiline: true) 85 | expect(renderer.render).to eq unindent(<<-EOS) 86 | +-------+-------+ 87 | |Multi |header2| 88 | |Header | | 89 | +-------+-------+ 90 | |First |1 | 91 | |Multi |2 | 92 | |Line | | 93 | |Content| | 94 | |Third |3 | 95 | +-------+-------+ 96 | EOS 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/resizing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, 'resizing' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'when expanding' do 11 | context 'even columns' do 12 | let(:options) { {width: 16, resize: true} } 13 | 14 | it 'resizes each column' do 15 | expect(renderer.render).to eql unindent(<<-EOS) 16 | +----+----+----+ 17 | |h1 |h2 |h3 | 18 | +----+----+----+ 19 | |a1 |a2 |a3 | 20 | |b1 |b2 |b3 | 21 | +----+----+----+ 22 | EOS 23 | end 24 | end 25 | 26 | context 'even columns with extra width' do 27 | let(:header) { ['h1', 'h2', 'h3', 'h4'] } 28 | let(:rows) { [['a1','a2','a3','a4'], ['b1','b2','b3','b4']] } 29 | let(:options) { {width: 21, resize: true} } 30 | 31 | it 'resizes each column' do 32 | expect(renderer.render).to eql unindent(<<-EOS) 33 | +----+----+----+----+ 34 | |h1 |h2 |h3 |h4 | 35 | +----+----+----+----+ 36 | |a1 |a2 |a3 |a4 | 37 | |b1 |b2 |b3 |b4 | 38 | +----+----+----+----+ 39 | EOS 40 | end 41 | end 42 | 43 | context 'uneven columns' do 44 | let(:header) { ['h1', 'h2', 'h3'] } 45 | let(:rows) { [['aaa1', 'aa2', 'aaaaaaa3'], ['b1', 'b2', 'b3']] } 46 | let(:options) { {width: 32, resize: true} } 47 | 48 | it 'resizes each column' do 49 | expect(renderer.render).to eql unindent(<<-EOS) 50 | +---------+-------+------------+ 51 | |h1 |h2 |h3 | 52 | +---------+-------+------------+ 53 | |aaa1 |aa2 |aaaaaaa3 | 54 | |b1 |b2 |b3 | 55 | +---------+-------+------------+ 56 | EOS 57 | end 58 | end 59 | end 60 | 61 | context 'when shrinking' do 62 | let(:header) { ['head1', 'head2'] } 63 | let(:rows) { [['aaaa1','aaaa2',], ['bbbb1','bbbb2']] } 64 | 65 | context 'even columns' do 66 | let(:options) { {width: 9, resize: true} } 67 | 68 | it 'resizes each column' do 69 | expect(renderer.render).to eql unindent(<<-EOS) 70 | +---+---+ 71 | |h… |h… | 72 | +---+---+ 73 | |a… |a… | 74 | |b… |b… | 75 | +---+---+ 76 | EOS 77 | end 78 | end 79 | 80 | context 'even columns with extra width' do 81 | let(:options) { {width: 10, resize: true} } 82 | 83 | it 'resizes each column' do 84 | expect(renderer.render).to eql unindent(<<-EOS) 85 | +----+---+ 86 | |he… |h… | 87 | +----+---+ 88 | |aa… |a… | 89 | |bb… |b… | 90 | +----+---+ 91 | EOS 92 | end 93 | end 94 | 95 | context 'uneven columns' do 96 | let(:header) { ['head1', 'head2', 'head3'] } 97 | let(:rows) { [['aaa1', 'aa2', 'aaaaaaa3'], ['b1', 'b2', 'b3']] } 98 | let(:options) { {width: 15, resize: true} } 99 | 100 | it 'resizes each column' do 101 | expect(renderer.render).to eql unindent(<<-EOS) 102 | +----+----+---+ 103 | |he… |he… |h… | 104 | +----+----+---+ 105 | |aaa1|aa2 |a… | 106 | |b1 |b2 |b3 | 107 | +----+----+---+ 108 | EOS 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/unit/padding_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'padding' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:table) { described_class.new(header, rows) } 7 | 8 | it 'sets specific padding' do 9 | expect(table.render(:ascii) { |renderer| 10 | renderer.padding.right = 2 11 | renderer.padding.top = 1 12 | }).to eq unindent(<<-EOS) 13 | +----+----+----+ 14 | | | | | 15 | |h1 |h2 |h3 | 16 | +----+----+----+ 17 | | | | | 18 | |a1 |a2 |a3 | 19 | | | | | 20 | |b1 |b2 |b3 | 21 | +----+----+----+ 22 | EOS 23 | end 24 | 25 | it 'sets padding for all' do 26 | expect(table.render(:ascii) { |renderer| 27 | renderer.padding= [1,2,1,2] 28 | }).to eq unindent(<<-EOS) 29 | +------+------+------+ 30 | | | | | 31 | | h1 | h2 | h3 | 32 | | | | | 33 | +------+------+------+ 34 | | | | | 35 | | a1 | a2 | a3 | 36 | | | | | 37 | | | | | 38 | | b1 | b2 | b3 | 39 | | | | | 40 | +------+------+------+ 41 | EOS 42 | end 43 | 44 | context 'with column width' do 45 | let(:column_widths) { [4,4,4] } 46 | 47 | it 'sets padding for all' do 48 | expect(table.render(:ascii) { |renderer| 49 | renderer.column_widths = column_widths 50 | renderer.padding= [1,2,1,2] 51 | }).to eq unindent(<<-EOS) 52 | +--------+--------+--------+ 53 | | | | | 54 | | h1 | h2 | h3 | 55 | | | | | 56 | +--------+--------+--------+ 57 | | | | | 58 | | a1 | a2 | a3 | 59 | | | | | 60 | | | | | 61 | | b1 | b2 | b3 | 62 | | | | | 63 | +--------+--------+--------+ 64 | EOS 65 | end 66 | end 67 | 68 | context 'with multi line text' do 69 | let(:header) { ['h1', 'head2'] } 70 | let(:rows) { [["Multi\nLine", "Text\nthat\nwraps"], 71 | ["Some\nother\ntext", 'Simple']] } 72 | 73 | context 'when wrapped' do 74 | it 'sets padding for all' do 75 | expect(table.render(:ascii) { |renderer| 76 | renderer.multiline = true 77 | renderer.padding= [1,2,1,2] 78 | }).to eq unindent(<<-EOS) 79 | +---------+----------+ 80 | | | | 81 | | h1 | head2 | 82 | | | | 83 | +---------+----------+ 84 | | | | 85 | | Multi | Text | 86 | | Line | that | 87 | | | wraps | 88 | | | | 89 | | | | 90 | | Some | Simple | 91 | | other | | 92 | | text | | 93 | | | | 94 | +---------+----------+ 95 | EOS 96 | end 97 | end 98 | 99 | context 'when escaped' do 100 | it 'sets padding for all' do 101 | expect(table.render(:ascii) { |renderer| 102 | renderer.multiline = false 103 | renderer.padding= [0,2,0,2] 104 | }).to eq unindent(<<-EOS) 105 | +---------------------+---------------------+ 106 | | h1 | head2 | 107 | +---------------------+---------------------+ 108 | | Multi\\nLine | Text\\nthat\\nwraps | 109 | | Some\\nother\\ntext | Simple | 110 | +---------------------+---------------------+ 111 | EOS 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/unit/orientation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table, 'orientation' do 4 | let(:header) { ['h1', 'h2', 'h3'] } 5 | let(:rows) { [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']] } 6 | let(:options) { { :orientation => orientation } } 7 | 8 | subject { described_class.new(header, rows, options) } 9 | 10 | context 'when illegal option' do 11 | let(:orientation) { :accross } 12 | 13 | it { expect { subject }.to raise_error(TTY::Table::InvalidOrientationError) } 14 | end 15 | 16 | context 'when horizontal' do 17 | let(:orientation) { :horizontal } 18 | 19 | it { expect(subject.orientation).to be_kind_of TTY::Table::Orientation } 20 | 21 | it { expect(subject.orientation.name).to eql(:horizontal) } 22 | 23 | it { expect(subject.header).to eql(header) } 24 | 25 | it 'preserves original rows' do 26 | expect(subject.to_a).to eql(subject.data) 27 | end 28 | 29 | context 'without border' do 30 | it 'displays table' do 31 | expect(subject.to_s).to eq unindent(<<-EOS) 32 | h1 h2 h3 33 | a1 a2 a3 34 | b1 b2 b3 35 | EOS 36 | end 37 | end 38 | 39 | context 'with border' do 40 | let(:renderer) { :ascii } 41 | 42 | it 'diplays table' do 43 | expect(subject.render(renderer)).to eq unindent(<<-EOS) 44 | +--+--+--+ 45 | |h1|h2|h3| 46 | +--+--+--+ 47 | |a1|a2|a3| 48 | |b1|b2|b3| 49 | +--+--+--+ 50 | EOS 51 | end 52 | end 53 | end 54 | 55 | context 'when vertical' do 56 | let(:orientation) { :vertical } 57 | 58 | it { expect(subject.orientation).to be_kind_of TTY::Table::Orientation } 59 | 60 | it { expect(subject.orientation.name).to eql :vertical } 61 | 62 | it { expect(subject.header).to be_empty } 63 | 64 | context 'with header' do 65 | it 'rotates original rows' do 66 | rotated_rows = [['h1','a1'],['h2','a2'],['h3','a3'], ['h1','b1'],['h2','b2'],['h3','b3']] 67 | expect(subject.to_a).to eql rotated_rows 68 | end 69 | 70 | context 'without border' do 71 | it 'displays table' do 72 | expect(subject.to_s).to eq unindent(<<-EOS) 73 | h1 a1 74 | h2 a2 75 | h3 a3 76 | h1 b1 77 | h2 b2 78 | h3 b3 79 | EOS 80 | end 81 | end 82 | 83 | context 'with border' do 84 | let(:renderer) { :ascii } 85 | 86 | it 'diplays table' do 87 | expect(subject.render(renderer)).to eq unindent(<<-EOS) 88 | +--+--+ 89 | |h1|a1| 90 | |h2|a2| 91 | |h3|a3| 92 | |h1|b1| 93 | |h2|b2| 94 | |h3|b3| 95 | +--+--+ 96 | EOS 97 | end 98 | end 99 | end 100 | 101 | context 'without header' do 102 | let(:header) { nil } 103 | 104 | it 'rotates original rows' do 105 | rotated_rows = [ 106 | ['1','a1'],['2','a2'],['3','a3'], 107 | ['1','b1'],['2','b2'],['3','b3'] 108 | ] 109 | expect(subject.to_a).to eql rotated_rows 110 | end 111 | 112 | context 'without border' do 113 | it 'displays table' do 114 | expect(subject.to_s).to eq unindent(<<-EOS) 115 | 1 a1 116 | 2 a2 117 | 3 a3 118 | 1 b1 119 | 2 b2 120 | 3 b3 121 | EOS 122 | end 123 | end 124 | 125 | context 'with border' do 126 | let(:renderer) { :ascii } 127 | 128 | it 'diplays table' do 129 | expect(subject.render(renderer)).to eq unindent(<<-EOS) 130 | +-+--+ 131 | |1|a1| 132 | |2|a2| 133 | |3|a3| 134 | |1|b1| 135 | |2|b2| 136 | |3|b3| 137 | +-+--+ 138 | EOS 139 | end 140 | end 141 | end 142 | end 143 | end # orientation 144 | -------------------------------------------------------------------------------- /lib/tty/table/header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | require_relative "error" 6 | require_relative "field" 7 | 8 | module TTY 9 | class Table 10 | # Convert an Array row into Header 11 | # 12 | # @return [TTY::Table::Header] 13 | # 14 | # @api private 15 | def to_header(row) 16 | Header.new(row) 17 | end 18 | 19 | # A set of header elements that correspond to values in each row 20 | class Header 21 | include Enumerable 22 | extend Forwardable 23 | 24 | def_delegators :@attributes, :join, :map, :map! 25 | 26 | # The header attributes 27 | # 28 | # @return [Array] 29 | # 30 | # @api private 31 | attr_reader :attributes 32 | alias :fields :attributes 33 | 34 | # Initialize a Header 35 | # 36 | # @return [undefined] 37 | # 38 | # @api public 39 | def initialize(attributes = []) 40 | @attributes = attributes.map { |attr| to_field(attr) } 41 | @attribute_for = Hash[@attributes.each_with_index.map.to_a] 42 | end 43 | 44 | # Iterate over each element in the vector 45 | # 46 | # @example 47 | # header = TTY::Table::Header.new [1,2,3] 48 | # header.each { |element| ... } 49 | # 50 | # @return [self] 51 | # 52 | # @api public 53 | def each 54 | return to_enum unless block_given? 55 | to_ary.each { |element| yield element } 56 | self 57 | end 58 | 59 | # Instantiates a new field 60 | # 61 | # @param [String,Hash] attribute 62 | # the attribute value to convert to field object 63 | # 64 | # @api public 65 | def to_field(attribute = nil) 66 | Field.new(attribute) 67 | end 68 | 69 | # Lookup a column in the header given a name 70 | # 71 | # @param [Integer, String] attribute 72 | # the attribute to look up by 73 | # 74 | # @api public 75 | def [](attribute) 76 | case attribute 77 | when Integer 78 | @attributes[attribute].value 79 | else 80 | @attribute_for.fetch(to_field(attribute)) do |header_name| 81 | raise UnknownAttributeError, 82 | "the header '#{header_name.value}' is unknown" 83 | end 84 | end 85 | end 86 | 87 | # Lookup attribute without evaluation 88 | # 89 | # @api public 90 | def call(attribute) 91 | @attributes[attribute] 92 | end 93 | 94 | # Set value at index 95 | # 96 | # @example 97 | # header[attribute] = value 98 | # 99 | # @api public 100 | def []=(attribute, value) 101 | attributes[attribute] = to_field(value) 102 | end 103 | 104 | # Size of the header 105 | # 106 | # @return [Integer] 107 | # 108 | # @api public 109 | def size 110 | to_ary.size 111 | end 112 | alias :length :size 113 | 114 | # Find maximum header height 115 | # 116 | # @return [Integer] 117 | # 118 | # @api public 119 | def height 120 | attributes.map { |field| field.height }.max 121 | end 122 | 123 | # Convert the Header into an Array 124 | # 125 | # @return [Array] 126 | # 127 | # @api public 128 | def to_ary 129 | attributes.map { |attr| attr.value if attr } 130 | end 131 | 132 | # Return the header elements in an array. 133 | # 134 | # @return [Array] 135 | # 136 | # @api public 137 | def to_a 138 | to_ary.dup 139 | end 140 | 141 | # Check if there are no elements. 142 | # 143 | # @return [Boolean] 144 | # 145 | # @api public 146 | def empty? 147 | to_ary.empty? 148 | end 149 | 150 | # Check if this header is equivalent to another header 151 | # 152 | # @return [Boolean] 153 | # 154 | # @api public 155 | def ==(other) 156 | to_a == other.to_a 157 | end 158 | alias :eql? :== 159 | 160 | # Provide an unique hash value 161 | # 162 | # @api public 163 | def to_hash 164 | to_a.hash 165 | end 166 | 167 | def inspect 168 | "#<#{self.class.name} fields=#{to_a}>" 169 | end 170 | end # Header 171 | end # Table 172 | end # TTY 173 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [v0.12.0] - 2020-09-20 4 | 5 | ### Changed 6 | * Optimize adding rows by Tim Craft(@timcraft) 7 | * Remove the equatable and necromancer dependencies 8 | * Update the pastel, tty-screen and strings dependencies 9 | * Change Border to use BorderDSL exclusively for handling options 10 | 11 | ### Fixed 12 | * Fix resize option raising an error for small terminal sizes by Katelyn Schiesser(@slowbro) 13 | * Fix Table::Indentation to check for map method support by @deemytch 14 | 15 | ## [v0.11.0] - 2019-08-01 16 | 17 | ### Add 18 | * Add ability to specify border separator as an Array, Proc value by Mitch VanDuyn(@catmando) 19 | 20 | ### Changed 21 | * Change Indentation#indent to stop mutating input 22 | * Change gemspec to load files directly 23 | * Change to freeze strings 24 | * Change to relax constraints on tty-screen & equatable 25 | * Change to remove upper boundary on bundler dev dependency 26 | 27 | ## [v0.10.0] - 2018-02-18 28 | 29 | ### Changed 30 | * Change to limit Ruby version to 2.0 31 | * Change to use strings instead of verse 32 | * Change Field to rely on Strings#display_width 33 | * Change Operations to stop accepting table at initialization 34 | * Change Operations#run_operations to #apply_to 35 | * Change Border to remove padding configuration 36 | * Change ColumnSet class to Columns stateless module 37 | * Change Indentation class to stateless module 38 | 39 | ### Fixed 40 | * Fix :resize option to honour :padding values 41 | 42 | ## [v0.9.0] - 2017-11-04 43 | 44 | ### Changed 45 | * Change ColumnSet class to Columns 46 | * Change gemset to require Ruby >= 2.0.0 47 | * Change to update tty-screen dependency 48 | 49 | ## [v0.8.0] - 2017-02-27 50 | 51 | ### Changed 52 | * Change necromancer dependency to fix Ruby 2.4.0 53 | * Change to use relative paths to load files 54 | 55 | ## [v0.7.0] - 2017-01-15 56 | 57 | ### Changed 58 | * Change loading of dependencies and required files 59 | * Change pastel dependency version 60 | 61 | ## [v0.6.0] - 2016-10-26 62 | 63 | ### Changed 64 | * Change to use unicode-display_width dependency 65 | * Upgrade verse dependency 66 | 67 | ## [v0.5.0] - 2016-02-11 68 | 69 | ### Changed 70 | * Upgrade pastel & tty-screen dependencies 71 | * Remove unused parameters from Operations::Padding 72 | 73 | ### Fixed 74 | * Fix all warnings 75 | 76 | ## [v0.4.0] - 2015-09-20 77 | 78 | ### Changed 79 | * Update dependencies on tty-screen and pastel 80 | 81 | ## [v0.3.0] - 2015-07-06 82 | 83 | ### Changed 84 | * Change benchmarks to reflect API 85 | * Change dependency on tty-screen 86 | 87 | ## [v0.2.0] - 2015-03-30 88 | 89 | ### Added 90 | * Add UTF-8 support for operations 91 | * Add AlignmentSet for alignments storage 92 | * Add tests for multilne column widths 93 | 94 | ### Changed 95 | * Change Table each_with_index to iterate over rows 96 | * Change Alignment operation to use AlignmentSet 97 | * Change Columns to directly depend on table data 98 | * Change Indentation to stop relying on renderer 99 | * Change Border to accept padding as argument 100 | * Change and extract padding operation 101 | * Change Columns to ColumnConstraint and refactor enforce 102 | * Remove padding from wrapped operation to fully rely on Verse.wrap 103 | * Remove color renderer 104 | * Remove adjust_padding from Columns 105 | 106 | ### Fixed 107 | * Fix table rendering for UTF-8 content 108 | * Fix alignment to allow for individual field alignment 109 | * Fix bug with padding operation 110 | * Fix table border and content coloring 111 | * Fix bug with table rerendering to allow for multiple renders 112 | * Fix bug with ANSI codes in table content 113 | 114 | ## [v0.1.0] - 2015-02-08 115 | 116 | * Initial implementation and release 117 | 118 | [v0.12.0]: https://github.com/piotrmurach/tty-table/compare/v0.11.0...v0.12.0 119 | [v0.11.0]: https://github.com/piotrmurach/tty-table/compare/v0.10.0...v0.11.0 120 | [v0.10.0]: https://github.com/piotrmurach/tty-table/compare/v0.9.0...v0.10.0 121 | [v0.9.0]: https://github.com/piotrmurach/tty-table/compare/v0.8.0...v0.9.0 122 | [v0.8.0]: https://github.com/piotrmurach/tty-table/compare/v0.7.0...v0.8.0 123 | [v0.7.0]: https://github.com/piotrmurach/tty-table/compare/v0.6.0...v0.7.0 124 | [v0.6.0]: https://github.com/piotrmurach/tty-table/compare/v0.5.0...v0.6.0 125 | [v0.5.0]: https://github.com/piotrmurach/tty-table/compare/v0.4.0...v0.5.0 126 | [v0.4.0]: https://github.com/piotrmurach/tty-table/compare/v0.3.0...v0.4.0 127 | [v0.3.0]: https://github.com/piotrmurach/tty-table/compare/v0.2.0...v0.3.0 128 | [v0.2.0]: https://github.com/piotrmurach/tty-table/compare/v0.1.0...v0.2.0 129 | [v0.1.0]: https://github.com/piotrmurach/tty-table/compare/v0.1.0 130 | -------------------------------------------------------------------------------- /lib/tty/table/field.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "strings" 4 | 5 | module TTY 6 | class Table 7 | # A class that represents a unique element in a table. 8 | # 9 | # Used internally by {Table::Header} and {Table::Row} to 10 | # define internal structure. 11 | # 12 | # @api private 13 | class Field 14 | # The value inside the field 15 | # 16 | # @api public 17 | attr_accessor :value 18 | 19 | # The formatted value inside the field used for display 20 | # 21 | # @api public 22 | attr_accessor :content 23 | 24 | # Number of columns this field spans. Defaults to 1. 25 | # 26 | # @api public 27 | attr_reader :colspan 28 | 29 | # Number of rows this field spans. Defaults to 1. 30 | # 31 | # @api public 32 | attr_reader :rowspan 33 | 34 | # The field alignment 35 | # 36 | # @api public 37 | attr_reader :alignment 38 | 39 | # Initialize a Field 40 | # 41 | # @example 42 | # field = TTY::Table::Field.new "a1" 43 | # field.value # => a1 44 | # 45 | # @example 46 | # field = TTY::Table::Field.new value: "a1" 47 | # field.value # => a1 48 | # 49 | # @example 50 | # field = TTY::Table::Field.new value: "a1", alignment: :center 51 | # field.value # => a1 52 | # field.alignment # => :center 53 | # 54 | # @api private 55 | def initialize(value) 56 | @value, options = extract_options(value) 57 | @content = @value.to_s 58 | @width = options[:width] 59 | @alignment = options.fetch(:alignment, nil) 60 | @colspan = options.fetch(:colspan, 1) 61 | @rowspan = options.fetch(:rowspan, 1) 62 | end 63 | 64 | # Extract options and set value 65 | # 66 | # @api private 67 | def extract_options(value) 68 | if value.is_a?(Hash) 69 | options = value 70 | value = options.fetch(:value) 71 | else 72 | options = {} 73 | end 74 | [value, options] 75 | end 76 | 77 | # Reset to original value 78 | # 79 | # @api public 80 | def reset! 81 | @content = @value.to_s 82 | end 83 | 84 | # The content width 85 | # 86 | # @api public 87 | def width 88 | @width || Strings::Align.display_width(@content) 89 | end 90 | 91 | # Return number of lines this value spans. 92 | # 93 | # A distinction is being made between escaped and non-escaped strings. 94 | # 95 | # @return [Array[String]] 96 | # 97 | # @api public 98 | def lines 99 | escaped = content.scan(/(\\n|\\t|\\r)/) 100 | escaped.empty? ? content.split(/\n/, -1) : [content] 101 | end 102 | 103 | # If the string contains unescaped new lines then the longest token 104 | # deterimines the actual field length. 105 | # 106 | # @return [Integer] 107 | # 108 | # @api public 109 | def length 110 | (lines.map do |line| 111 | Strings::Align.display_width(line) 112 | end << 0).max 113 | end 114 | 115 | # Extract the number of lines this value spans 116 | # 117 | # @return [Integer] 118 | # 119 | # @api public 120 | def height 121 | lines.size 122 | end 123 | 124 | def chars 125 | content.chars 126 | end 127 | 128 | # Return field content 129 | # 130 | # @return [String] 131 | # 132 | # @api public 133 | def to_s 134 | content 135 | end 136 | 137 | # Compare fields for equality of value attribute 138 | # 139 | # @return [Boolean] 140 | # 141 | # @api public 142 | def eql?(other) 143 | instance_of?(other.class) && value.eql?(other.value) 144 | end 145 | 146 | # Compare fields for equivalence of value attribute 147 | # 148 | # @return [Boolean] 149 | # 150 | # @api public 151 | def ==(other) 152 | other.is_a?(self.class) && value == other.value 153 | end 154 | 155 | # Inspect this instance attributes 156 | # 157 | # @return [String] 158 | # 159 | # @api public 160 | def inspect 161 | "#<#{self.class.name} value=#{value.inspect} " \ 162 | "rowspan=#{rowspan.inspect} colspan=#{colspan.inspect}>" 163 | end 164 | 165 | # Hash for this instance and its attributes 166 | # 167 | # @return [Numeric] 168 | # 169 | # @api public 170 | def hash 171 | [self.class, value].hash 172 | end 173 | end # Field 174 | end # Table 175 | end # TTY 176 | -------------------------------------------------------------------------------- /spec/unit/renderer/ascii/padding_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Table::Renderer::ASCII, 'padding' do 4 | let(:header) { ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra'] } 5 | let(:rows) { [['id', 'int(11)', 'YES', 'nil', 'NULL', '']] } 6 | let(:table) { TTY::Table.new(header, rows) } 7 | 8 | subject(:renderer) { described_class.new(table, options) } 9 | 10 | context 'with left & right padding' do 11 | let(:options) { {padding: [0,1,0,1]} } 12 | 13 | it 'pads each field' do 14 | expect(renderer.render).to eql <<-EOS.chomp 15 | +-------+---------+------+-----+---------+-------+ 16 | | Field | Type | Null | Key | Default | Extra | 17 | +-------+---------+------+-----+---------+-------+ 18 | | id | int(11) | YES | nil | NULL | | 19 | +-------+---------+------+-----+---------+-------+ 20 | EOS 21 | end 22 | end 23 | 24 | context 'with top & bottom padding' do 25 | let(:options) { {padding: [1,0,1,0], multiline: true} } 26 | 27 | it 'pads each field' do 28 | expect(renderer.render).to eql <<-EOS.chomp 29 | +-----+-------+----+---+-------+-----+ 30 | | | | | | | | 31 | |Field|Type |Null|Key|Default|Extra| 32 | | | | | | | | 33 | +-----+-------+----+---+-------+-----+ 34 | | | | | | | | 35 | |id |int(11)|YES |nil|NULL | | 36 | | | | | | | | 37 | +-----+-------+----+---+-------+-----+ 38 | EOS 39 | end 40 | end 41 | 42 | context 'with full padding' do 43 | let(:options) { {padding: [1,1,1,1], multiline: true} } 44 | 45 | it 'pads each field' do 46 | expect(renderer.render).to eql unindent(<<-EOS) 47 | +-------+---------+------+-----+---------+-------+ 48 | | | | | | | | 49 | | Field | Type | Null | Key | Default | Extra | 50 | | | | | | | | 51 | +-------+---------+------+-----+---------+-------+ 52 | | | | | | | | 53 | | id | int(11) | YES | nil | NULL | | 54 | | | | | | | | 55 | +-------+---------+------+-----+---------+-------+ 56 | EOS 57 | end 58 | end 59 | 60 | context "with multiline content padding" do 61 | it "pads around fields" do 62 | padding = [1,2,1,2] 63 | table = TTY::Table.new header: ['header1', 'header2'] 64 | table << ["a1\na1\na1",'a2'] 65 | table << ["b1","b2\nb2"] 66 | renderer = described_class.new(table, padding: padding, multiline: true) 67 | expect(renderer.render).to eql unindent(<<-EOS) 68 | +-----------+-----------+ 69 | | | | 70 | | header1 | header2 | 71 | | | | 72 | +-----------+-----------+ 73 | | | | 74 | | a1 | a2 | 75 | | a1 | | 76 | | a1 | | 77 | | | | 78 | | | | 79 | | b1 | b2 | 80 | | | b2 | 81 | | | | 82 | +-----------+-----------+ 83 | EOS 84 | end 85 | end 86 | 87 | context "with resize option" do 88 | it "pads fields correctly" do 89 | table = TTY::Table.new(header: [ "Column 1", "Column 2", "Column 3"]) do |t| 90 | t << [ "11", "12", "13" ] 91 | t << [ "21", "22", "23" ] 92 | t << [ "31", "32", "33" ] 93 | end 94 | 95 | renderer = TTY::Table::Renderer::ASCII.new(table, padding: [1,1,1,1], resize: true, width: 50) 96 | expect(renderer.render).to eql unindent(<<-EOS) 97 | +----------------+---------------+---------------+ 98 | | | | | 99 | | Column 1 | Column 2 | Column 3 | 100 | | | | | 101 | +----------------+---------------+---------------+ 102 | | | | | 103 | | 11 | 12 | 13 | 104 | | | | | 105 | | | | | 106 | | 21 | 22 | 23 | 107 | | | | | 108 | | | | | 109 | | 31 | 32 | 33 | 110 | | | | | 111 | +----------------+---------------+---------------+ 112 | EOS 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/tty/table/column_constraint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "border/null" 4 | require_relative "columns" 5 | require_relative "error" 6 | 7 | module TTY 8 | class Table 9 | # A class responsible for enforcing column constraints. 10 | # 11 | # Used internally by {Renderer::Basic} to enforce correct column widths. 12 | # 13 | # @api private 14 | class ColumnConstraint 15 | MIN_WIDTH = 1 16 | 17 | BORDER_WIDTH = 1 18 | 19 | attr_reader :table 20 | 21 | attr_reader :renderer 22 | 23 | # Initialize a Columns 24 | # 25 | # @param [TTY::Table] table 26 | # 27 | # @param [TTY::Table::Renderer] renderer 28 | # 29 | # @api public 30 | def initialize(table, renderer) 31 | @table = table 32 | @renderer = renderer 33 | end 34 | 35 | # Estimate outside border size 36 | # 37 | # @return [Integer] 38 | # 39 | # @api public 40 | def outside_border_size 41 | renderer.border_class == TTY::Table::Border::Null ? 0 : 2 42 | end 43 | 44 | # Total border size 45 | # 46 | # @return [Integer] 47 | # 48 | # @api public 49 | def border_size 50 | BORDER_WIDTH * (table.columns_size - 1) + outside_border_size 51 | end 52 | 53 | # Measure total padding size for a table 54 | # 55 | # @return [Integer] 56 | # 57 | # @api public 58 | def padding_size 59 | padding = renderer.padding 60 | (padding.left + padding.right) * table.columns_count 61 | end 62 | 63 | # Estimate minimum table width to be able to display content 64 | # 65 | # @return [Integer] 66 | # 67 | # @api public 68 | def minimum_width 69 | table.columns_size * MIN_WIDTH + border_size 70 | end 71 | 72 | # Return column's natural unconstrained widths 73 | # 74 | # @return [Integer] 75 | # 76 | # @api public 77 | def natural_width 78 | renderer.column_widths.inject(0, &:+) + border_size + padding_size 79 | end 80 | 81 | # Return the constrained column widths. 82 | # 83 | # Account for table field widths and any user defined 84 | # constraints on the table width. 85 | # 86 | # @api public 87 | def enforce 88 | assert_minimum_width 89 | padding = renderer.padding 90 | 91 | if natural_width <= renderer.width 92 | if renderer.resize 93 | expand_column_widths 94 | else 95 | renderer.column_widths.map do |width| 96 | padding.left + width + padding.right 97 | end 98 | end 99 | else 100 | if renderer.resize 101 | shrink 102 | else 103 | rotate 104 | end 105 | end 106 | end 107 | 108 | private 109 | 110 | # Rotate table to vertical orientation and print information to stdout 111 | # 112 | # @api private 113 | def rotate 114 | Kernel.warn "The table size exceeds the currently set width." \ 115 | "Defaulting to vertical orientation." 116 | table.orientation = :vertical 117 | table.rotate 118 | Columns.widths_from(table) 119 | end 120 | 121 | # Expand column widths to match the requested width 122 | # 123 | # @api private 124 | def expand_column_widths 125 | columns_count = table.columns_count 126 | max_width = renderer.width 127 | extra_column_width = ((max_width - natural_width) / columns_count.to_f).floor 128 | 129 | widths = (0...columns_count).reduce([]) do |lengths, col| 130 | lengths << renderer.column_widths[col] + extra_column_width 131 | end 132 | distribute_extra_width(widths) 133 | end 134 | 135 | # Shrink column widths to match the requested width 136 | # 137 | # @api private 138 | def shrink 139 | column_size = table.columns_size 140 | ratio = ((natural_width - renderer.width) / column_size.to_f).ceil 141 | 142 | widths = (0...column_size).reduce([]) do |lengths, col| 143 | width = (renderer.column_widths[col] - ratio) 144 | # basically ruby 2.4 Numeric#clamp 145 | width = width < minimum_width ? minimum_width : width 146 | width = width > renderer.width ? renderer.width : width 147 | lengths << width 148 | end 149 | distribute_extra_width(widths) 150 | end 151 | 152 | # Assert minimum width for the table content 153 | # 154 | # @raise [TTY::ResizeError] 155 | # 156 | # @api private 157 | def assert_minimum_width 158 | width = renderer.width 159 | return unless width <= minimum_width 160 | raise ResizeError, "Table's width is too small to contain the content " \ 161 | "(min width #{minimum_width}, currently set #{width})" 162 | end 163 | 164 | # Distribute remaining width to meet the total width requirement. 165 | # 166 | # @param [Array[Integer]] widths 167 | # 168 | # @api private 169 | def distribute_extra_width(widths) 170 | column_size = table.columns_size 171 | # TODO - add padding size to fully check extra width 172 | extra_width = renderer.width - (widths.reduce(:+) + border_size) 173 | per_field_width = extra_width / column_size 174 | remaining_width = extra_width % column_size 175 | extra = [1] * remaining_width + [0] * (column_size - remaining_width) 176 | 177 | widths.map.with_index do |width, index| 178 | width + per_field_width + extra[index] 179 | end 180 | end 181 | end # ColumnConstraint 182 | end # Table 183 | end # TTY 184 | --------------------------------------------------------------------------------