tabulo 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ackrc +10 -0
- data/.gitignore +12 -0
- data/.rdoc_options +22 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +249 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1353 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/_config.yml +1 -0
- data/assets/social_media_preview/table.png +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/tabulo.rb +10 -0
- data/lib/tabulo/border.rb +164 -0
- data/lib/tabulo/cell.rb +155 -0
- data/lib/tabulo/cell_data.rb +14 -0
- data/lib/tabulo/column.rb +114 -0
- data/lib/tabulo/deprecation.rb +33 -0
- data/lib/tabulo/exceptions.rb +12 -0
- data/lib/tabulo/row.rb +51 -0
- data/lib/tabulo/table.rb +763 -0
- data/lib/tabulo/util.rb +45 -0
- data/lib/tabulo/version.rb +3 -0
- data/tabulo.gemspec +43 -0
- metadata +227 -0
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require "rake-version"
|
4
|
+
require "yard"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
task default: :spec
|
9
|
+
|
10
|
+
RakeVersion::Tasks.new do |v|
|
11
|
+
v.copy "lib/tabulo/version.rb"
|
12
|
+
v.copy "README.md", all: true
|
13
|
+
end
|
14
|
+
|
15
|
+
YARD::Rake::YardocTask.new do |t|
|
16
|
+
t.options = ["--markup-provider=redcarpet", "--markup=markdown"]
|
17
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.0
|
data/_config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
theme: jekyll-theme-dinky
|
Binary file
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "tabulo"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/tabulo.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "tabulo/deprecation"
|
2
|
+
require "tabulo/exceptions"
|
3
|
+
require "tabulo/util"
|
4
|
+
require "tabulo/table"
|
5
|
+
require "tabulo/version"
|
6
|
+
require "tabulo/row"
|
7
|
+
require "tabulo/cell_data"
|
8
|
+
require "tabulo/cell"
|
9
|
+
require "tabulo/column"
|
10
|
+
require "tabulo/border"
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Tabulo
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
class Border
|
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
|
+
|
12
|
+
STYLES = {
|
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
|
+
),
|
62
|
+
}
|
63
|
+
|
64
|
+
# @!visibility private
|
65
|
+
def self.from(initializer, styler = nil)
|
66
|
+
new(**options(initializer).merge(styler: styler))
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!visibility private
|
70
|
+
def horizontal_rule(column_widths, position = :bottom)
|
71
|
+
left, center, right, segment =
|
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]
|
77
|
+
when :top
|
78
|
+
[@corner_top_left, @tee_top, @corner_top_right, @edge_top]
|
79
|
+
when :middle
|
80
|
+
[@tee_left, @intersection, @tee_right, @divider_horizontal]
|
81
|
+
when :bottom
|
82
|
+
[@corner_bottom_left, @tee_bottom, @corner_bottom_right, @edge_bottom]
|
83
|
+
end
|
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
|
+
|
90
|
+
style("#{left}#{segments.join(center)}#{right}")
|
91
|
+
end
|
92
|
+
|
93
|
+
# @!visibility private
|
94
|
+
def join_cell_contents(cells)
|
95
|
+
styled_divider_vertical = style(@divider_vertical)
|
96
|
+
styled_edge_left = style(@edge_left)
|
97
|
+
styled_edge_right = style(@edge_right)
|
98
|
+
styled_edge_left + cells.join(styled_divider_vertical) + styled_edge_right
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def self.options(kind)
|
104
|
+
opts = STYLES[kind]
|
105
|
+
return opts.to_h if opts
|
106
|
+
raise InvalidBorderError
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [nil, #to_proc] styler (nil) A lambda or other callable object taking
|
110
|
+
# a single parameter, representing a section of the table's borders (which for this purpose
|
111
|
+
# include any horizontal and vertical lines inside the table), and returning a string.
|
112
|
+
# If passed <tt>nil</tt>, then no additional styling will be applied to borders. If passed a
|
113
|
+
# callable, then that callable will be called for each border section, with the
|
114
|
+
# resulting string rendered in place of that border. The extra width of the string returned by the
|
115
|
+
# {styler} is not taken into consideration by the internal table rendering calculations
|
116
|
+
# Thus it can be used to apply ANSI escape codes to border characters, to colour the borders
|
117
|
+
# for example, without breaking the table formatting.
|
118
|
+
# @return [Border] a new {Border}
|
119
|
+
def initialize(
|
120
|
+
corner_top_left: "",
|
121
|
+
corner_top_right: "",
|
122
|
+
corner_bottom_right: "",
|
123
|
+
corner_bottom_left: "",
|
124
|
+
edge_top: "",
|
125
|
+
edge_right: "",
|
126
|
+
edge_bottom: "",
|
127
|
+
edge_left: "",
|
128
|
+
tee_top: "",
|
129
|
+
tee_right: "",
|
130
|
+
tee_bottom: "",
|
131
|
+
tee_left: "",
|
132
|
+
divider_vertical: "",
|
133
|
+
divider_horizontal: "",
|
134
|
+
intersection: "",
|
135
|
+
styler: nil)
|
136
|
+
|
137
|
+
@corner_top_left = corner_top_left
|
138
|
+
@corner_top_right = corner_top_right
|
139
|
+
@corner_bottom_right = corner_bottom_right
|
140
|
+
@corner_bottom_left = corner_bottom_left
|
141
|
+
|
142
|
+
@edge_top = edge_top
|
143
|
+
@edge_right = edge_right
|
144
|
+
@edge_bottom = edge_bottom
|
145
|
+
@edge_left = edge_left
|
146
|
+
|
147
|
+
@tee_top = tee_top
|
148
|
+
@tee_right = tee_right
|
149
|
+
@tee_bottom = tee_bottom
|
150
|
+
@tee_left = tee_left
|
151
|
+
|
152
|
+
@divider_vertical = divider_vertical
|
153
|
+
@divider_horizontal = divider_horizontal
|
154
|
+
|
155
|
+
@intersection = intersection
|
156
|
+
|
157
|
+
@styler = styler
|
158
|
+
end
|
159
|
+
|
160
|
+
def style(s)
|
161
|
+
(@styler && !s.empty?) ? @styler.call(s) : s
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/lib/tabulo/cell.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require "unicode/display_width"
|
2
|
+
|
3
|
+
module Tabulo
|
4
|
+
|
5
|
+
# Represents a single cell within the body of a {Table}.
|
6
|
+
class Cell
|
7
|
+
|
8
|
+
# @return the underlying value for this Cell
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
# @!visibility private
|
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
|
+
|
24
|
+
@alignment = alignment
|
25
|
+
@cell_data = cell_data
|
26
|
+
@formatter = formatter
|
27
|
+
@left_padding = left_padding
|
28
|
+
@padding_character = padding_character
|
29
|
+
@right_padding = right_padding
|
30
|
+
@styler = styler
|
31
|
+
@truncation_indicator = truncation_indicator
|
32
|
+
@value = value
|
33
|
+
@width = width
|
34
|
+
end
|
35
|
+
|
36
|
+
# @!visibility private
|
37
|
+
def height
|
38
|
+
subcells.size
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
def padded_truncated_subcells(target_height)
|
43
|
+
total_padding_amount = @left_padding + @right_padding
|
44
|
+
truncated = (height > target_height)
|
45
|
+
(0...target_height).map do |subcell_index|
|
46
|
+
append_truncator = (truncated && (total_padding_amount != 0) && (subcell_index + 1 == target_height))
|
47
|
+
padded_subcell(subcell_index, append_truncator)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [String] the content of the Cell, after applying the formatter for this Column (but
|
52
|
+
# without applying any wrapping or the styler).
|
53
|
+
def formatted_content
|
54
|
+
@formatted_content ||= apply_formatter
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
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
|
+
|
78
|
+
def subcells
|
79
|
+
@subcells ||= calculate_subcells
|
80
|
+
end
|
81
|
+
|
82
|
+
def padded_subcell(subcell_index, append_truncator)
|
83
|
+
lpad = @padding_character * @left_padding
|
84
|
+
rpad =
|
85
|
+
if append_truncator
|
86
|
+
styled_truncation_indicator(subcell_index) + padding(@right_padding - 1)
|
87
|
+
else
|
88
|
+
padding(@right_padding)
|
89
|
+
end
|
90
|
+
inner = subcell_index < height ? subcells[subcell_index] : padding(@width)
|
91
|
+
"#{lpad}#{inner}#{rpad}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def padding(amount)
|
95
|
+
@padding_character * amount
|
96
|
+
end
|
97
|
+
|
98
|
+
def styled_truncation_indicator(line_index)
|
99
|
+
apply_styler(@truncation_indicator, line_index)
|
100
|
+
end
|
101
|
+
|
102
|
+
def calculate_subcells
|
103
|
+
line_index = 0
|
104
|
+
formatted_content.split($/, -1).flat_map do |substr|
|
105
|
+
subsubcells, subsubcell, subsubcell_width = [], String.new(""), 0
|
106
|
+
|
107
|
+
substr.scan(/\X/).each do |grapheme_cluster|
|
108
|
+
grapheme_cluster_width = Unicode::DisplayWidth.of(grapheme_cluster)
|
109
|
+
if subsubcell_width + grapheme_cluster_width > @width
|
110
|
+
subsubcells << style_and_align_cell_content(subsubcell, line_index)
|
111
|
+
subsubcell_width = 0
|
112
|
+
subsubcell.clear
|
113
|
+
line_index += 1
|
114
|
+
end
|
115
|
+
|
116
|
+
subsubcell << grapheme_cluster
|
117
|
+
subsubcell_width += grapheme_cluster_width
|
118
|
+
end
|
119
|
+
|
120
|
+
subsubcells << style_and_align_cell_content(subsubcell, line_index)
|
121
|
+
line_index += 1
|
122
|
+
subsubcells
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def style_and_align_cell_content(content, line_index)
|
127
|
+
padding = Util.max(@width - Unicode::DisplayWidth.of(content), 0)
|
128
|
+
left_padding, right_padding =
|
129
|
+
case real_alignment
|
130
|
+
when :center
|
131
|
+
half_padding = padding / 2
|
132
|
+
[padding - half_padding, half_padding]
|
133
|
+
when :left
|
134
|
+
[0, padding]
|
135
|
+
when :right
|
136
|
+
[padding, 0]
|
137
|
+
end
|
138
|
+
|
139
|
+
"#{' ' * left_padding}#{apply_styler(content, line_index)}#{' ' * right_padding}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def real_alignment
|
143
|
+
return @alignment unless @alignment == :auto
|
144
|
+
|
145
|
+
case @value
|
146
|
+
when Numeric
|
147
|
+
:right
|
148
|
+
when TrueClass, FalseClass
|
149
|
+
:center
|
150
|
+
else
|
151
|
+
:left
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -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
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Tabulo
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
class Column
|
5
|
+
|
6
|
+
attr_accessor :width
|
7
|
+
attr_reader :header
|
8
|
+
attr_reader :index
|
9
|
+
attr_reader :left_padding
|
10
|
+
attr_reader :right_padding
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
align_body:,
|
14
|
+
align_header:,
|
15
|
+
extractor:,
|
16
|
+
formatter:,
|
17
|
+
header:,
|
18
|
+
header_styler:,
|
19
|
+
index:,
|
20
|
+
left_padding:,
|
21
|
+
padding_character:,
|
22
|
+
right_padding:,
|
23
|
+
styler:,
|
24
|
+
truncation_indicator:,
|
25
|
+
width:)
|
26
|
+
|
27
|
+
@align_body = align_body
|
28
|
+
@align_header = align_header
|
29
|
+
@extractor = extractor
|
30
|
+
@formatter = formatter
|
31
|
+
@header = header
|
32
|
+
@index = index
|
33
|
+
@left_padding = left_padding
|
34
|
+
@right_padding = right_padding
|
35
|
+
|
36
|
+
@header_styler =
|
37
|
+
if header_styler
|
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
|
46
|
+
else
|
47
|
+
-> (_, str) { str }
|
48
|
+
end
|
49
|
+
|
50
|
+
@padding_character = padding_character
|
51
|
+
@styler = styler || -> (_, s) { s }
|
52
|
+
@truncation_indicator = truncation_indicator
|
53
|
+
@width = width
|
54
|
+
end
|
55
|
+
|
56
|
+
def header_cell
|
57
|
+
if @header_styler.arity >= 3
|
58
|
+
cell_data = CellData.new(nil, nil, @index)
|
59
|
+
end
|
60
|
+
Cell.new(
|
61
|
+
alignment: @align_header,
|
62
|
+
cell_data: cell_data,
|
63
|
+
formatter: -> (s) { s },
|
64
|
+
left_padding: @left_padding,
|
65
|
+
padding_character: @padding_character,
|
66
|
+
right_padding: @right_padding,
|
67
|
+
styler: @header_styler,
|
68
|
+
truncation_indicator: @truncation_indicator,
|
69
|
+
value: @header,
|
70
|
+
width: @width,
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
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
|
78
|
+
Cell.new(
|
79
|
+
alignment: @align_body,
|
80
|
+
cell_data: cell_data,
|
81
|
+
formatter: @formatter,
|
82
|
+
left_padding: @left_padding,
|
83
|
+
padding_character: @padding_character,
|
84
|
+
right_padding: @right_padding,
|
85
|
+
styler: @styler,
|
86
|
+
truncation_indicator: @truncation_indicator,
|
87
|
+
value: body_cell_value(source, row_index: row_index, column_index: column_index),
|
88
|
+
width: @width,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
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)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|