tabulo 2.3.3 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|