tabulo 2.3.3 → 2.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +330 -96
- data/Rakefile +6 -1
- data/VERSION +1 -1
- data/assets/social_media_preview/table.png +0 -0
- data/lib/tabulo.rb +1 -0
- data/lib/tabulo/border.rb +66 -102
- data/lib/tabulo/cell.rb +56 -19
- data/lib/tabulo/cell_data.rb +14 -0
- data/lib/tabulo/column.rb +52 -6
- data/lib/tabulo/row.rb +8 -5
- data/lib/tabulo/table.rb +268 -110
- data/lib/tabulo/util.rb +20 -0
- data/lib/tabulo/version.rb +1 -1
- data/tabulo.gemspec +3 -3
- metadata +11 -10
data/Rakefile
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
require "rake-version"
|
4
|
+
require "yard"
|
4
5
|
|
5
6
|
RSpec::Core::RakeTask.new(:spec)
|
6
7
|
|
7
|
-
task :
|
8
|
+
task default: :spec
|
8
9
|
|
9
10
|
RakeVersion::Tasks.new do |v|
|
10
11
|
v.copy "lib/tabulo/version.rb"
|
11
12
|
v.copy "README.md", all: true
|
12
13
|
end
|
14
|
+
|
15
|
+
YARD::Rake::YardocTask.new do |t|
|
16
|
+
t.options = ["--markup-provider=redcarpet", "--markup=markdown"]
|
17
|
+
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.6.1
|
Binary file
|
data/lib/tabulo.rb
CHANGED
data/lib/tabulo/border.rb
CHANGED
@@ -3,118 +3,77 @@ module Tabulo
|
|
3
3
|
# @!visibility private
|
4
4
|
class Border
|
5
5
|
|
6
|
+
Style = Struct.new(
|
7
|
+
:corner_top_left, :corner_top_right, :corner_bottom_right, :corner_bottom_left,
|
8
|
+
:edge_top, :edge_right, :edge_bottom, :edge_left,
|
9
|
+
:tee_top, :tee_right, :tee_bottom, :tee_left,
|
10
|
+
:divider_vertical, :divider_horizontal, :intersection)
|
11
|
+
|
6
12
|
STYLES = {
|
7
|
-
ascii:
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
corner_top_right: "",
|
57
|
-
corner_bottom_right: "",
|
58
|
-
corner_bottom_left: "",
|
59
|
-
edge_top: "─",
|
60
|
-
edge_right: "",
|
61
|
-
edge_bottom: "─",
|
62
|
-
edge_left: "",
|
63
|
-
tee_top: " ",
|
64
|
-
tee_right: "",
|
65
|
-
tee_bottom: " ",
|
66
|
-
tee_left: "",
|
67
|
-
divider_vertical: " ",
|
68
|
-
divider_horizontal: "─",
|
69
|
-
intersection: " ",
|
70
|
-
},
|
71
|
-
markdown: {
|
72
|
-
corner_top_left: "",
|
73
|
-
corner_top_right: "",
|
74
|
-
corner_bottom_right: "",
|
75
|
-
corner_bottom_left: "",
|
76
|
-
edge_top: "",
|
77
|
-
edge_right: "|",
|
78
|
-
edge_bottom: "",
|
79
|
-
edge_left: "|",
|
80
|
-
tee_top: "",
|
81
|
-
tee_right: "|",
|
82
|
-
tee_bottom: "",
|
83
|
-
tee_left: "|",
|
84
|
-
divider_vertical: "|",
|
85
|
-
divider_horizontal: "-",
|
86
|
-
intersection: "|",
|
87
|
-
},
|
88
|
-
modern: {
|
89
|
-
corner_top_left: "┌",
|
90
|
-
corner_top_right: "┐",
|
91
|
-
corner_bottom_right: "┘",
|
92
|
-
corner_bottom_left: "└",
|
93
|
-
edge_top: "─",
|
94
|
-
edge_right: "│",
|
95
|
-
edge_bottom: "─",
|
96
|
-
edge_left: "│",
|
97
|
-
tee_top: "┬",
|
98
|
-
tee_right: "┤",
|
99
|
-
tee_bottom: "┴",
|
100
|
-
tee_left: "├",
|
101
|
-
divider_vertical: "│",
|
102
|
-
divider_horizontal: "─",
|
103
|
-
intersection: "┼",
|
104
|
-
},
|
105
|
-
blank: {
|
106
|
-
},
|
13
|
+
ascii:
|
14
|
+
Style.new(
|
15
|
+
"+", "+", "+", "+",
|
16
|
+
"-", "|", "-", "|",
|
17
|
+
"+", "+", "+", "+",
|
18
|
+
"|", "-", "+",
|
19
|
+
),
|
20
|
+
classic:
|
21
|
+
Style.new(
|
22
|
+
"+", "+", "", "",
|
23
|
+
"-", "|", "", "|",
|
24
|
+
"+", "+", "", "+",
|
25
|
+
"|", "-", "+",
|
26
|
+
),
|
27
|
+
reduced_ascii:
|
28
|
+
Style.new(
|
29
|
+
"", "", "", "",
|
30
|
+
"-", "", "-", "",
|
31
|
+
" ", "", " ", "",
|
32
|
+
" ", "-", " ",
|
33
|
+
),
|
34
|
+
reduced_modern:
|
35
|
+
Style.new(
|
36
|
+
"", "", "", "",
|
37
|
+
"─", "", "─", "",
|
38
|
+
" ", "", " ", "",
|
39
|
+
" ", "─", " ",
|
40
|
+
),
|
41
|
+
markdown:
|
42
|
+
Style.new(
|
43
|
+
"", "", "", "",
|
44
|
+
"", "|", "", "|",
|
45
|
+
"", "|", "", "|",
|
46
|
+
"|", "-", "|",
|
47
|
+
),
|
48
|
+
modern:
|
49
|
+
Style.new(
|
50
|
+
"┌", "┐", "┘", "└",
|
51
|
+
"─", "│", "─", "│",
|
52
|
+
"┬", "┤", "┴", "├",
|
53
|
+
"│", "─", "┼",
|
54
|
+
),
|
55
|
+
blank:
|
56
|
+
Style.new(
|
57
|
+
"", "", "", "",
|
58
|
+
"", "", "", "",
|
59
|
+
"", "", "", "",
|
60
|
+
"", "", "",
|
61
|
+
),
|
107
62
|
}
|
108
63
|
|
109
64
|
# @!visibility private
|
110
65
|
def self.from(initializer, styler = nil)
|
111
|
-
new(options(initializer).merge(styler: styler))
|
66
|
+
new(**options(initializer).merge(styler: styler))
|
112
67
|
end
|
113
68
|
|
114
69
|
# @!visibility private
|
115
70
|
def horizontal_rule(column_widths, position = :bottom)
|
116
71
|
left, center, right, segment =
|
117
72
|
case position
|
73
|
+
when :title_top
|
74
|
+
[@corner_top_left, @edge_top, @corner_top_right, @edge_top]
|
75
|
+
when :title_bottom
|
76
|
+
[@tee_left, @tee_top, @tee_right, @edge_top]
|
118
77
|
when :top
|
119
78
|
[@corner_top_left, @tee_top, @corner_top_right, @edge_top]
|
120
79
|
when :middle
|
@@ -123,6 +82,11 @@ module Tabulo
|
|
123
82
|
[@corner_bottom_left, @tee_bottom, @corner_bottom_right, @edge_bottom]
|
124
83
|
end
|
125
84
|
segments = column_widths.map { |width| segment * width }
|
85
|
+
|
86
|
+
# Prevent weird bottom edge of title if segments empty but right/left not empty, as in
|
87
|
+
# Markdown border.
|
88
|
+
left = right = "" if segments.all?(&:empty?)
|
89
|
+
|
126
90
|
style("#{left}#{segments.join(center)}#{right}")
|
127
91
|
end
|
128
92
|
|
@@ -138,7 +102,7 @@ module Tabulo
|
|
138
102
|
|
139
103
|
def self.options(kind)
|
140
104
|
opts = STYLES[kind]
|
141
|
-
return opts if opts
|
105
|
+
return opts.to_h if opts
|
142
106
|
raise InvalidBorderError
|
143
107
|
end
|
144
108
|
|
data/lib/tabulo/cell.rb
CHANGED
@@ -9,14 +9,28 @@ module Tabulo
|
|
9
9
|
attr_reader :value
|
10
10
|
|
11
11
|
# @!visibility private
|
12
|
-
def initialize(
|
13
|
-
|
14
|
-
|
12
|
+
def initialize(
|
13
|
+
alignment:,
|
14
|
+
cell_data:,
|
15
|
+
formatter:,
|
16
|
+
left_padding:,
|
17
|
+
padding_character:,
|
18
|
+
right_padding:,
|
19
|
+
styler:,
|
20
|
+
truncation_indicator:,
|
21
|
+
value:,
|
22
|
+
width:)
|
23
|
+
|
15
24
|
@alignment = alignment
|
16
|
-
@
|
25
|
+
@cell_data = cell_data
|
26
|
+
@formatter = formatter
|
27
|
+
@left_padding = left_padding
|
28
|
+
@padding_character = padding_character
|
29
|
+
@right_padding = right_padding
|
17
30
|
@styler = styler
|
18
31
|
@truncation_indicator = truncation_indicator
|
19
|
-
@
|
32
|
+
@value = value
|
33
|
+
@width = width
|
20
34
|
end
|
21
35
|
|
22
36
|
# @!visibility private
|
@@ -25,34 +39,53 @@ module Tabulo
|
|
25
39
|
end
|
26
40
|
|
27
41
|
# @!visibility private
|
28
|
-
def padded_truncated_subcells(target_height
|
29
|
-
total_padding_amount =
|
42
|
+
def padded_truncated_subcells(target_height)
|
43
|
+
total_padding_amount = @left_padding + @right_padding
|
30
44
|
truncated = (height > target_height)
|
31
45
|
(0...target_height).map do |subcell_index|
|
32
46
|
append_truncator = (truncated && (total_padding_amount != 0) && (subcell_index + 1 == target_height))
|
33
|
-
padded_subcell(subcell_index,
|
47
|
+
padded_subcell(subcell_index, append_truncator)
|
34
48
|
end
|
35
49
|
end
|
36
50
|
|
37
51
|
# @return [String] the content of the Cell, after applying the formatter for this Column (but
|
38
52
|
# without applying any wrapping or the styler).
|
39
53
|
def formatted_content
|
40
|
-
@formatted_content ||=
|
54
|
+
@formatted_content ||= apply_formatter
|
41
55
|
end
|
42
56
|
|
43
57
|
private
|
44
58
|
|
59
|
+
def apply_formatter
|
60
|
+
if @formatter.arity == 2
|
61
|
+
@formatter.call(@value, @cell_data)
|
62
|
+
else
|
63
|
+
@formatter.call(@value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def apply_styler(content, line_index)
|
68
|
+
case @styler.arity
|
69
|
+
when 4
|
70
|
+
@styler.call(@value, content, @cell_data, line_index)
|
71
|
+
when 3
|
72
|
+
@styler.call(@value, content, @cell_data)
|
73
|
+
else
|
74
|
+
@styler.call(@value, content)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
45
78
|
def subcells
|
46
79
|
@subcells ||= calculate_subcells
|
47
80
|
end
|
48
81
|
|
49
|
-
def padded_subcell(subcell_index,
|
50
|
-
lpad = @padding_character *
|
82
|
+
def padded_subcell(subcell_index, append_truncator)
|
83
|
+
lpad = @padding_character * @left_padding
|
51
84
|
rpad =
|
52
85
|
if append_truncator
|
53
|
-
styled_truncation_indicator + padding(
|
86
|
+
styled_truncation_indicator(subcell_index) + padding(@right_padding - 1)
|
54
87
|
else
|
55
|
-
padding(
|
88
|
+
padding(@right_padding)
|
56
89
|
end
|
57
90
|
inner = subcell_index < height ? subcells[subcell_index] : padding(@width)
|
58
91
|
"#{lpad}#{inner}#{rpad}"
|
@@ -62,31 +95,35 @@ module Tabulo
|
|
62
95
|
@padding_character * amount
|
63
96
|
end
|
64
97
|
|
65
|
-
def styled_truncation_indicator
|
66
|
-
|
98
|
+
def styled_truncation_indicator(line_index)
|
99
|
+
apply_styler(@truncation_indicator, line_index)
|
67
100
|
end
|
68
101
|
|
69
102
|
def calculate_subcells
|
103
|
+
line_index = 0
|
70
104
|
formatted_content.split($/, -1).flat_map do |substr|
|
71
105
|
subsubcells, subsubcell, subsubcell_width = [], String.new(""), 0
|
72
106
|
|
73
107
|
substr.scan(/\X/).each do |grapheme_cluster|
|
74
108
|
grapheme_cluster_width = Unicode::DisplayWidth.of(grapheme_cluster)
|
75
109
|
if subsubcell_width + grapheme_cluster_width > @width
|
76
|
-
subsubcells << style_and_align_cell_content(subsubcell)
|
110
|
+
subsubcells << style_and_align_cell_content(subsubcell, line_index)
|
77
111
|
subsubcell_width = 0
|
78
112
|
subsubcell.clear
|
113
|
+
line_index += 1
|
79
114
|
end
|
80
115
|
|
81
116
|
subsubcell << grapheme_cluster
|
82
117
|
subsubcell_width += grapheme_cluster_width
|
83
118
|
end
|
84
119
|
|
85
|
-
subsubcells << style_and_align_cell_content(subsubcell)
|
120
|
+
subsubcells << style_and_align_cell_content(subsubcell, line_index)
|
121
|
+
line_index += 1
|
122
|
+
subsubcells
|
86
123
|
end
|
87
124
|
end
|
88
125
|
|
89
|
-
def style_and_align_cell_content(content)
|
126
|
+
def style_and_align_cell_content(content, line_index)
|
90
127
|
padding = Util.max(@width - Unicode::DisplayWidth.of(content), 0)
|
91
128
|
left_padding, right_padding =
|
92
129
|
case real_alignment
|
@@ -99,7 +136,7 @@ module Tabulo
|
|
99
136
|
[padding, 0]
|
100
137
|
end
|
101
138
|
|
102
|
-
"#{' ' * left_padding}#{
|
139
|
+
"#{' ' * left_padding}#{apply_styler(content, line_index)}#{' ' * right_padding}"
|
103
140
|
end
|
104
141
|
|
105
142
|
def real_alignment
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Tabulo
|
2
|
+
|
3
|
+
# Contains information about a particular {Cell} in the {Table}.
|
4
|
+
#
|
5
|
+
# @attr source [Object] The member of this {Cell}'s {Table}'s underlying enumerable from which
|
6
|
+
# this {Cell}'s {Row} was derived.
|
7
|
+
# @attr row_index [Integer] The positional index of the {Cell}'s {Row}. The topmost {Row} of the
|
8
|
+
# {Table} has index 0, the next has index 1, etc.. The header row(s) are not counted for the purpose
|
9
|
+
# of this numbering.
|
10
|
+
# @attr column_index [Integer] The positional index of the {Cell}'s {Column}. The leftmost {Column}
|
11
|
+
# of the {Table} has index 0, the next has index 1, etc..
|
12
|
+
CellData = Struct.new(:source, :row_index, :column_index)
|
13
|
+
|
14
|
+
end
|
data/lib/tabulo/column.rb
CHANGED
@@ -5,6 +5,9 @@ module Tabulo
|
|
5
5
|
|
6
6
|
attr_accessor :width
|
7
7
|
attr_reader :header
|
8
|
+
attr_reader :index
|
9
|
+
attr_reader :left_padding
|
10
|
+
attr_reader :right_padding
|
8
11
|
|
9
12
|
def initialize(
|
10
13
|
align_body:,
|
@@ -13,7 +16,10 @@ module Tabulo
|
|
13
16
|
formatter:,
|
14
17
|
header:,
|
15
18
|
header_styler:,
|
19
|
+
index:,
|
20
|
+
left_padding:,
|
16
21
|
padding_character:,
|
22
|
+
right_padding:,
|
17
23
|
styler:,
|
18
24
|
truncation_indicator:,
|
19
25
|
width:)
|
@@ -23,12 +29,22 @@ module Tabulo
|
|
23
29
|
@extractor = extractor
|
24
30
|
@formatter = formatter
|
25
31
|
@header = header
|
32
|
+
@index = index
|
33
|
+
@left_padding = left_padding
|
34
|
+
@right_padding = right_padding
|
26
35
|
|
27
36
|
@header_styler =
|
28
37
|
if header_styler
|
29
|
-
|
38
|
+
case header_styler.arity
|
39
|
+
when 3
|
40
|
+
-> (_, str, cell_data, line_index) { header_styler.call(str, cell_data.column_index, line_index) }
|
41
|
+
when 2
|
42
|
+
-> (_, str, cell_data) { header_styler.call(str, cell_data.column_index) }
|
43
|
+
else
|
44
|
+
-> (_, str) { header_styler.call(str) }
|
45
|
+
end
|
30
46
|
else
|
31
|
-
-> (_,
|
47
|
+
-> (_, str) { str }
|
32
48
|
end
|
33
49
|
|
34
50
|
@padding_character = padding_character
|
@@ -38,10 +54,16 @@ module Tabulo
|
|
38
54
|
end
|
39
55
|
|
40
56
|
def header_cell
|
57
|
+
if @header_styler.arity >= 3
|
58
|
+
cell_data = CellData.new(nil, nil, @index)
|
59
|
+
end
|
41
60
|
Cell.new(
|
42
61
|
alignment: @align_header,
|
62
|
+
cell_data: cell_data,
|
43
63
|
formatter: -> (s) { s },
|
64
|
+
left_padding: @left_padding,
|
44
65
|
padding_character: @padding_character,
|
66
|
+
right_padding: @right_padding,
|
45
67
|
styler: @header_styler,
|
46
68
|
truncation_indicator: @truncation_indicator,
|
47
69
|
value: @header,
|
@@ -49,20 +71,44 @@ module Tabulo
|
|
49
71
|
)
|
50
72
|
end
|
51
73
|
|
52
|
-
def body_cell(source)
|
74
|
+
def body_cell(source, row_index:, column_index:)
|
75
|
+
if body_cell_data_required?
|
76
|
+
cell_data = CellData.new(source, row_index, @index)
|
77
|
+
end
|
53
78
|
Cell.new(
|
54
79
|
alignment: @align_body,
|
80
|
+
cell_data: cell_data,
|
55
81
|
formatter: @formatter,
|
82
|
+
left_padding: @left_padding,
|
56
83
|
padding_character: @padding_character,
|
84
|
+
right_padding: @right_padding,
|
57
85
|
styler: @styler,
|
58
86
|
truncation_indicator: @truncation_indicator,
|
59
|
-
value: body_cell_value(source),
|
87
|
+
value: body_cell_value(source, row_index: row_index, column_index: column_index),
|
60
88
|
width: @width,
|
61
89
|
)
|
62
90
|
end
|
63
91
|
|
64
|
-
def body_cell_value(source)
|
65
|
-
@extractor.
|
92
|
+
def body_cell_value(source, row_index:, column_index:)
|
93
|
+
if @extractor.arity == 2
|
94
|
+
@extractor.call(source, row_index)
|
95
|
+
else
|
96
|
+
@extractor.call(source)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def padded_width
|
101
|
+
width + total_padding
|
102
|
+
end
|
103
|
+
|
104
|
+
def total_padding
|
105
|
+
@left_padding + @right_padding
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def body_cell_data_required?
|
111
|
+
@cell_data_required ||= (@styler.arity == 3 || @formatter.arity == 2)
|
66
112
|
end
|
67
113
|
end
|
68
114
|
end
|