tabulo 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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