tabulo 2.6.0

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.
@@ -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
@@ -0,0 +1 @@
1
+ theme: jekyll-theme-dinky
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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