From ace478cb99edc4d1b53855c34df50e590fba83c1 Mon Sep 17 00:00:00 2001 From: Shlomi Zadok Date: Tue, 27 Sep 2016 16:27:08 +0300 Subject: [PATCH] Fixes #3520 - respect special chars width --- hammer_cli.gemspec | 1 + lib/hammer_cli/output/adapter/table.rb | 2 + lib/hammer_cli/output/utils.rb | 43 +++++++++ lib/hammer_cli/table_print/column.rb | 19 ++++ lib/hammer_cli/table_print/formatter.rb | 18 ++++ test/unit/output/adapter/table_test.rb | 114 +++++++++++++++++++++--- 6 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 lib/hammer_cli/output/utils.rb create mode 100644 lib/hammer_cli/table_print/column.rb create mode 100644 lib/hammer_cli/table_print/formatter.rb diff --git a/hammer_cli.gemspec b/hammer_cli.gemspec index e17a137af..5d48862c2 100644 --- a/hammer_cli.gemspec +++ b/hammer_cli.gemspec @@ -25,6 +25,7 @@ EOF s.add_dependency 'clamp', '~> 1.0' s.add_dependency 'logging' + s.add_dependency 'unicode-display_width' s.add_dependency 'awesome_print' s.add_dependency 'table_print' s.add_dependency 'highline' diff --git a/lib/hammer_cli/output/adapter/table.rb b/lib/hammer_cli/output/adapter/table.rb index 4f315a307..f77693838 100644 --- a/lib/hammer_cli/output/adapter/table.rb +++ b/lib/hammer_cli/output/adapter/table.rb @@ -1,5 +1,7 @@ require 'table_print' require File.join(File.dirname(__FILE__), 'wrapper_formatter') +require 'hammer_cli/table_print/formatter' +require 'hammer_cli/table_print/column' module HammerCLI::Output::Adapter diff --git a/lib/hammer_cli/output/utils.rb b/lib/hammer_cli/output/utils.rb new file mode 100644 index 000000000..ae9030013 --- /dev/null +++ b/lib/hammer_cli/output/utils.rb @@ -0,0 +1,43 @@ +require 'unicode/display_width' + +module HammerCLI + module Output + module Utils + def self.real_length(value) + decolorized = value.gsub(/\033\[[^m]*m/, '') + Unicode::DisplayWidth.of(decolorized) + end + + def self.real_char_length(ch) + Unicode::DisplayWidth.of(ch) + end + + def self.real_truncate(value, required_size) + size = 0 + index = 0 + has_colors = false + in_color = false + value.each_char do |ch| + if in_color + in_color = false if ch == "m" + elsif ch == "\e" + has_colors = in_color = true + else + increment = real_char_length(ch) + if size + increment > required_size + if has_colors + return value[0..index-1] + "\e[0m", size + else + return value[0..index-1], size + end + else + size += increment + end + end + index += 1 + end + return value, size + end + end + end +end diff --git a/lib/hammer_cli/table_print/column.rb b/lib/hammer_cli/table_print/column.rb new file mode 100644 index 000000000..4260ce08d --- /dev/null +++ b/lib/hammer_cli/table_print/column.rb @@ -0,0 +1,19 @@ +require 'hammer_cli/output/utils' + +module TablePrint + class Column + def data_width + if multibyte_count + [ + HammerCLI::Output::Utils.real_length(name), + Array(data).compact.collect(&:to_s).collect{|m| HammerCLI::Output::Utils.real_length(m) }.max + ].compact.max || 0 + else + [ + name.length, + Array(data).compact.collect(&:to_s).collect(&:length).max + ].compact.max || 0 + end + end + end +end diff --git a/lib/hammer_cli/table_print/formatter.rb b/lib/hammer_cli/table_print/formatter.rb new file mode 100644 index 000000000..bdabffbbc --- /dev/null +++ b/lib/hammer_cli/table_print/formatter.rb @@ -0,0 +1,18 @@ +require 'hammer_cli/output/utils' + +module TablePrint + class FixedWidthFormatter + def format(value) + value = value.to_s + padding = width - HammerCLI::Output::Utils.real_length(value) + if padding >= 0 + value += (" " * padding) + else + value, real_length = HammerCLI::Output::Utils.real_truncate(value, width-3) + value += '...' + value += ' ' if real_length < (width - 3) + end + value + end + end +end diff --git a/test/unit/output/adapter/table_test.rb b/test/unit/output/adapter/table_test.rb index 0ec7b4972..3f4af509c 100644 --- a/test/unit/output/adapter/table_test.rb +++ b/test/unit/output/adapter/table_test.rb @@ -19,12 +19,20 @@ [field_name] } + let(:red) { "\e[1;31m" } + let(:reset) { "\e[0m" } + let(:record) { { :id => 1, :firstname => "John", :lastname => "Doe", + :two_column_chars => "文字漢字", + :czech_chars => "žluťoučký kůň", + :colorized_name => "#{red}John#{reset}", :fullname => "John Doe", - :long => "SomeVeryLongString" + :long => "SomeVeryLongString", + :colorized_long => "#{red}SomeVeryLongString#{reset}", + :two_column_long => "文字-Kanji-漢字-Hanja-漢字" } } let(:data) { HammerCLI::Output::RecordCollection.new [record] } let(:empty_data) { HammerCLI::Output::RecordCollection.new [] } @@ -90,6 +98,86 @@ context "column width" do + it "calculates correct width of two-column characters" do + first_field = Fields::Field.new(:path => [:two_column_chars], :label => "Some characters") + fields = [first_field, field_lastname] + + expected_output = [ + "----------------|---------", + "SOME CHARACTERS | LASTNAME", + "----------------|---------", + "文字漢字 | Doe ", + "----------------|---------", + "" + ].join("\n") + + proc { adapter.print_collection(fields, data) }.must_output(expected_output) + end + + it "calculates correct width of czech characters" do + first_field = Fields::Field.new(:path => [:czech_chars], :label => "Some characters") + fields = [first_field, field_lastname] + + expected_output = [ + "----------------|---------", + "SOME CHARACTERS | LASTNAME", + "----------------|---------", + "žluťoučký kůň | Doe ", + "----------------|---------", + "" + ].join("\n") + + proc { adapter.print_collection(fields, data) }.must_output(expected_output) + end + + it "calculates correct width of colorized strings" do + first_field = Fields::Field.new(:path => [:colorized_name], :label => "Colorized name") + fields = [first_field, field_lastname] + + expected_output = [ + "---------------|---------", + "COLORIZED NAME | LASTNAME", + "---------------|---------", + "John | Doe ", + "---------------|---------", + "" + ].join("\n").gsub('John', "#{red}John#{reset}") + + proc { adapter.print_collection(fields, data) }.must_output(expected_output) + end + + it "truncates two-column characters when it exceeds maximum width" do + first_field = Fields::Field.new(:path => [:two_column_long], :label => "Some characters", :max_width => 16) + fields = [first_field, field_lastname] + + expected_output = [ + "-----------------|---------", + "SOME CHARACTERS | LASTNAME", + "-----------------|---------", + "文字-Kanji-漢... | Doe ", + "-----------------|---------", + "" + ].join("\n") + + proc { adapter.print_collection(fields, data) }.must_output(expected_output) + end + + it "truncates colorized string string when it exceeds maximum width" do + first_field = Fields::Field.new(:path => [:colorized_long], :label => "Long", :max_width => 10) + fields = [first_field, field_lastname] + + expected_output = [ + "-----------|---------", + "LONG | LASTNAME", + "-----------|---------", + "SomeVer... | Doe ", + "-----------|---------", + "" + ].join("\n").gsub('SomeVer', "#{red}SomeVer#{reset}") + + proc { adapter.print_collection(fields, data) }.must_output(expected_output) + end + it "truncates string when it exceeds maximum width" do first_field = Fields::Field.new(:path => [:long], :label => "Long", :max_width => 10) fields = [first_field, field_lastname] @@ -122,18 +210,18 @@ proc { adapter.print_collection(fields, data) }.must_output(expected_output) end - it "sets width to the longest column name when no data" do - first_field = Fields::Field.new(:path => [:long], :label => "VeryLongTableHeaderName") - fields = [first_field, field_lastname] - - expected_output = [ - "------------------------|---------", - "VERYLONGTABLEHEADERNAME | LASTNAME", - "------------------------|---------", - "" - ].join("\n") - proc { adapter.print_collection(fields, empty_data) }.must_output(expected_output) - end + it "sets width to the longest column name when no data" do + first_field = Fields::Field.new(:path => [:long], :label => "VeryLongTableHeaderName") + fields = [first_field, field_lastname] + + expected_output = [ + "------------------------|---------", + "VERYLONGTABLEHEADERNAME | LASTNAME", + "------------------------|---------", + "" + ].join("\n") + proc { adapter.print_collection(fields, empty_data) }.must_output(expected_output) + end it "sets certain width" do first_field = Fields::Field.new(:path => [:long], :label => "Long", :width => 25)