prawn-table-continued 1.0.0.rc1

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +340 -0
  4. data/GPLv3 +674 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +56 -0
  7. data/lib/prawn/table/cell/image.rb +69 -0
  8. data/lib/prawn/table/cell/in_table.rb +33 -0
  9. data/lib/prawn/table/cell/span_dummy.rb +93 -0
  10. data/lib/prawn/table/cell/subtable.rb +66 -0
  11. data/lib/prawn/table/cell/text.rb +155 -0
  12. data/lib/prawn/table/cell.rb +787 -0
  13. data/lib/prawn/table/cells.rb +261 -0
  14. data/lib/prawn/table/column_width_calculator.rb +182 -0
  15. data/lib/prawn/table/version.rb +5 -0
  16. data/lib/prawn/table.rb +711 -0
  17. data/manual/contents.rb +13 -0
  18. data/manual/example_helper.rb +8 -0
  19. data/manual/images/prawn.png +0 -0
  20. data/manual/images/stef.jpg +0 -0
  21. data/manual/table/basic_block.rb +53 -0
  22. data/manual/table/before_rendering_page.rb +26 -0
  23. data/manual/table/cell_border_lines.rb +24 -0
  24. data/manual/table/cell_borders_and_bg.rb +31 -0
  25. data/manual/table/cell_dimensions.rb +36 -0
  26. data/manual/table/cell_text.rb +38 -0
  27. data/manual/table/column_widths.rb +30 -0
  28. data/manual/table/content_and_subtables.rb +39 -0
  29. data/manual/table/creation.rb +27 -0
  30. data/manual/table/filtering.rb +36 -0
  31. data/manual/table/flow_and_header.rb +17 -0
  32. data/manual/table/image_cells.rb +33 -0
  33. data/manual/table/position.rb +29 -0
  34. data/manual/table/row_colors.rb +20 -0
  35. data/manual/table/span.rb +30 -0
  36. data/manual/table/style.rb +33 -0
  37. data/manual/table/table.rb +52 -0
  38. data/manual/table/width.rb +27 -0
  39. data/prawn-table.gemspec +36 -0
  40. data/spec/cell_spec.rb +652 -0
  41. data/spec/extensions/encoding_helpers.rb +11 -0
  42. data/spec/extensions/file_fixture_helper.rb +15 -0
  43. data/spec/fixtures/files/prawn.png +0 -0
  44. data/spec/spec_helper.rb +50 -0
  45. data/spec/table/span_dummy_spec.rb +26 -0
  46. data/spec/table_spec.rb +1626 -0
  47. metadata +234 -0
@@ -0,0 +1,261 @@
1
+ # encoding: utf-8
2
+
3
+ # cells.rb: Methods for accessing rows, columns, and cells of a Prawn::Table.
4
+ #
5
+ # Copyright December 2009, Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ class Table
11
+ # Selects the given rows (0-based) for styling. Returns a Cells object --
12
+ # see the documentation on Cells for things you can do with cells.
13
+ #
14
+ def rows(row_spec)
15
+ cells.rows(row_spec)
16
+ end
17
+ alias_method :row, :rows
18
+
19
+ # Selects the given columns (0-based) for styling. Returns a Cells object
20
+ # -- see the documentation on Cells for things you can do with cells.
21
+ #
22
+ def columns(col_spec)
23
+ cells.columns(col_spec)
24
+ end
25
+ alias_method :column, :columns
26
+
27
+ # Represents a selection of cells to be styled. Operations on a CellProxy
28
+ # can be chained, and cell properties can be set one-for-all on the proxy.
29
+ #
30
+ # To set vertical borders only:
31
+ #
32
+ # table.cells.borders = [:left, :right]
33
+ #
34
+ # To highlight a rectangular area of the table:
35
+ #
36
+ # table.rows(1..3).columns(2..4).background_color = 'ff0000'
37
+ #
38
+ class Cells < Array
39
+
40
+ def fits_on_current_page?(offset, ref_bounds)
41
+ # an empty row array means it definitely fits
42
+ return true if self.empty?
43
+
44
+ height_with_span < (self[0,0].y + offset) - ref_bounds.absolute_bottom
45
+ end
46
+
47
+ # @group Experimental API
48
+
49
+ # Limits selection to the given row or rows. +row_spec+ can be anything
50
+ # that responds to the === operator selecting a set of 0-based row
51
+ # numbers; most commonly a number or a range.
52
+ #
53
+ # table.row(0) # selects first row
54
+ # table.rows(3..4) # selects rows four and five
55
+ #
56
+ def rows(row_spec)
57
+ index_cells unless defined?(@indexed) && @indexed
58
+ row_spec = transform_spec(row_spec, @first_row, @row_count)
59
+ Cells.new(@rows[row_spec] ||= select { |c|
60
+ row_spec.respond_to?(:include?) ?
61
+ row_spec.include?(c.row) : row_spec === c.row })
62
+ end
63
+ alias_method :row, :rows
64
+
65
+ # Returns the number of rows in the list.
66
+ #
67
+ def row_count
68
+ index_cells unless defined?(@indexed) && @indexed
69
+ @row_count
70
+ end
71
+
72
+ # Limits selection to the given column or columns. +col_spec+ can be
73
+ # anything that responds to the === operator selecting a set of 0-based
74
+ # column numbers; most commonly a number or a range.
75
+ #
76
+ # table.column(0) # selects first column
77
+ # table.columns(3..4) # selects columns four and five
78
+ #
79
+ def columns(col_spec)
80
+ index_cells unless defined?(@indexed) && @indexed
81
+ col_spec = transform_spec(col_spec, @first_column, @column_count)
82
+ Cells.new(@columns[col_spec] ||= select { |c|
83
+ col_spec.respond_to?(:include?) ?
84
+ col_spec.include?(c.column) : col_spec === c.column })
85
+ end
86
+ alias_method :column, :columns
87
+
88
+ # Returns the number of columns in the list.
89
+ #
90
+ def column_count
91
+ index_cells unless defined?(@indexed) && @indexed
92
+ @column_count
93
+ end
94
+
95
+ # Allows you to filter the given cells by arbitrary properties.
96
+ #
97
+ # table.column(4).filter { |cell| cell.content =~ /Yes/ }.
98
+ # background_color = '00ff00'
99
+ #
100
+ def filter(&block)
101
+ Cells.new(select(&block))
102
+ end
103
+
104
+ # Retrieves a cell based on its 0-based row and column. Returns an
105
+ # individual Cell, not a Cells collection.
106
+ #
107
+ # table.cells[0, 0].content # => "First cell content"
108
+ #
109
+ def [](row, col)
110
+ return nil if empty?
111
+ index_cells unless defined?(@indexed) && @indexed
112
+ row_array, col_array = @rows[@first_row + row] || [], @columns[@first_column + col] || []
113
+ if row_array.length < col_array.length
114
+ row_array.find { |c| c.column == @first_column + col }
115
+ else
116
+ col_array.find { |c| c.row == @first_row + row }
117
+ end
118
+ end
119
+
120
+ # Puts a cell in the collection at the given position. Internal use only.
121
+ #
122
+ def []=(row, col, cell) # :nodoc:
123
+ cell.extend(Cell::InTable)
124
+ cell.row = row
125
+ cell.column = col
126
+
127
+ if defined?(@indexed) && @indexed
128
+ (@rows[row] ||= []) << cell
129
+ (@columns[col] ||= []) << cell
130
+ @first_row = row if !@first_row || row < @first_row
131
+ @first_column = col if !@first_column || col < @first_column
132
+ @row_count = @rows.size
133
+ @column_count = @columns.size
134
+ end
135
+
136
+ self << cell
137
+ end
138
+
139
+ # Supports setting multiple properties at once.
140
+ #
141
+ # table.cells.style(:padding => 0, :border_width => 2)
142
+ #
143
+ # is the same as:
144
+ #
145
+ # table.cells.padding = 0
146
+ # table.cells.border_width = 2
147
+ #
148
+ # You can also pass a block, which will be called for each cell in turn.
149
+ # This allows you to set more complicated properties:
150
+ #
151
+ # table.cells.style { |cell| cell.border_width += 12 }
152
+ #
153
+ def style(options={}, &block)
154
+ each do |cell|
155
+ next if cell.is_a?(Cell::SpanDummy)
156
+ cell.style(options, &block)
157
+ end
158
+ end
159
+
160
+ # Returns the total width of all columns in the selected set.
161
+ #
162
+ def width
163
+ ColumnWidthCalculator.new(self).natural_widths.inject(0, &:+)
164
+ end
165
+
166
+ # Returns minimum width required to contain cells in the set.
167
+ #
168
+ def min_width
169
+ aggregate_cell_values(:column, :avg_spanned_min_width, :max)
170
+ end
171
+
172
+ # Returns maximum width that can contain cells in the set.
173
+ #
174
+ def max_width
175
+ aggregate_cell_values(:column, :max_width_ignoring_span, :max)
176
+ end
177
+
178
+ # Returns the total height of all rows in the selected set.
179
+ #
180
+ def height
181
+ aggregate_cell_values(:row, :height_ignoring_span, :max)
182
+ end
183
+
184
+ # Returns the total height of all rows in the selected set
185
+ # including spanned cells if the cell is the master cell
186
+ #
187
+ def height_with_span
188
+ aggregate_cell_values(:row, :height, :max)
189
+ end
190
+
191
+ # Supports setting arbitrary properties on a group of cells.
192
+ #
193
+ # table.cells.row(3..6).background_color = 'cc0000'
194
+ #
195
+ def method_missing(id, *args, &block)
196
+ if id.to_s =~ /=\z/
197
+ each { |c| c.send(id, *args, &block) if c.respond_to?(id) }
198
+ else
199
+ super
200
+ end
201
+ end
202
+
203
+ protected
204
+
205
+ # Defers indexing until rows() or columns() is actually called on the
206
+ # Cells object. Without this, we would needlessly index the leaf nodes of
207
+ # the object graph, the ones that are only there to be iterated over.
208
+ #
209
+ # Make sure to call this before using @rows or @columns.
210
+ #
211
+ def index_cells
212
+ @rows = {}
213
+ @columns = {}
214
+
215
+ each do |cell|
216
+ @rows[cell.row] ||= []
217
+ @rows[cell.row] << cell
218
+
219
+ @columns[cell.column] ||= []
220
+ @columns[cell.column] << cell
221
+ end
222
+
223
+ @first_row = @rows.keys.min
224
+ @first_column = @columns.keys.min
225
+
226
+ @row_count = @rows.size
227
+ @column_count = @columns.size
228
+
229
+ @indexed = true
230
+ end
231
+
232
+ # Sum up a min/max value over rows or columns in the cells selected.
233
+ # Takes the min/max (per +aggregate+) of the result of sending +meth+ to
234
+ # each cell, grouped by +row_or_column+.
235
+ #
236
+ def aggregate_cell_values(row_or_column, meth, aggregate)
237
+ ColumnWidthCalculator.new(self).aggregate_cell_values(row_or_column, meth, aggregate)
238
+ end
239
+
240
+ # Transforms +spec+, a column / row specification, into an object that
241
+ # can be compared against a row or column number using ===. Normalizes
242
+ # negative indices to be positive, given a total size of +total+. The
243
+ # first row/column is indicated by +first+; this value is considered row
244
+ # or column 0.
245
+ #
246
+ def transform_spec(spec, first, total)
247
+ case spec
248
+ when Range
249
+ transform_spec(spec.begin, first, total) ..
250
+ transform_spec(spec.end, first, total)
251
+ when Integer
252
+ spec < 0 ? (first + total + spec) : first + spec
253
+ when Enumerable
254
+ spec.map { |x| first + x }
255
+ else # pass through
256
+ raise "Don't understand spec #{spec.inspect}"
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,182 @@
1
+ # encoding: utf-8
2
+
3
+ module Prawn
4
+ class Table
5
+ # @private
6
+ class ColumnWidthCalculator
7
+ def initialize(cells)
8
+ @cells = cells
9
+
10
+ @widths_by_column = Hash.new(0)
11
+ @rows_with_a_span_dummy = Hash.new(false)
12
+
13
+ #calculate for each row if it includes a Cell:SpanDummy
14
+ @cells.each do |cell|
15
+ @rows_with_a_span_dummy[cell.row] = true if cell.is_a?(Cell::SpanDummy)
16
+ end
17
+ end
18
+
19
+ # does this row include a Cell:SpanDummy?
20
+ #
21
+ # @param row - the row that should be checked for Cell:SpanDummy elements
22
+ #
23
+ def has_a_span_dummy?(row)
24
+ @rows_with_a_span_dummy[row]
25
+ end
26
+
27
+ # helper method
28
+ # column widths are stored in the values array
29
+ # a cell may span cells whose value is only partly given
30
+ # this function handles this special case
31
+ #
32
+ # @param values - The columns widths calculated up until now
33
+ # @param cell - The current cell
34
+ # @param index - The current column
35
+ # @param meth - Meth (min/max); used to calculate values to be filled
36
+ #
37
+ def fill_values_if_needed(values, cell, index, meth)
38
+ #have all spanned indices been filled with a value?
39
+ #e.g. values[0], values[1] and values[2] don't return nil given a index of 0 and a colspan of 3
40
+ number_of_nil_values = 0
41
+ cell.colspan.times do |i|
42
+ number_of_nil_values += 1 if values[index+i].nil?
43
+ end
44
+
45
+ #nothing to do? because
46
+ #a) all values are filled
47
+ return values if number_of_nil_values == 0
48
+ #b) no values are filled
49
+ return values if number_of_nil_values == cell.colspan
50
+ #c) I am not sure why this line is needed FIXXME
51
+ #some test cases manage to this line even though there is no dummy cell in the row
52
+ #I'm not sure if this is a sign for a further underlying bug.
53
+ return values unless has_a_span_dummy?(cell.row)
54
+ #fill up the values array
55
+
56
+ #calculate the new sum
57
+ new_sum = cell.send(meth) * cell.colspan
58
+ #substract any calculated values
59
+ cell.colspan.times do |i|
60
+ new_sum -= values[index+i] unless values[index+i].nil?
61
+ end
62
+
63
+ #calculate value for the remaining - not yet filled - cells.
64
+ new_value = new_sum.to_f / number_of_nil_values
65
+ #fill the not yet filled cells
66
+ cell.colspan.times do |i|
67
+ values[index+i] = new_value if values[index+i].nil?
68
+ end
69
+ return values
70
+ end
71
+
72
+ def natural_widths
73
+ #calculate natural column width for all rows that do not include a span dummy
74
+ @cells.each do |cell|
75
+ unless has_a_span_dummy?(cell.row)
76
+ @widths_by_column[cell.column] =
77
+ [@widths_by_column[cell.column], cell.width.to_f].max
78
+ end
79
+ end
80
+
81
+ #integrate natural column widths for all rows that do include a span dummy
82
+ @cells.each do |cell|
83
+ next unless has_a_span_dummy?(cell.row)
84
+ #the width of a SpanDummy cell will be calculated by the "mother" cell
85
+ next if cell.is_a?(Cell::SpanDummy)
86
+
87
+ if cell.colspan == 1
88
+ @widths_by_column[cell.column] =
89
+ [@widths_by_column[cell.column], cell.width.to_f].max
90
+ else
91
+ #calculate the current with of all cells that will be spanned by the current cell
92
+ current_width_of_spanned_cells =
93
+ @widths_by_column.to_a[cell.column..(cell.column + cell.colspan - 1)]
94
+ .collect{|key, value| value}.inject(0, :+)
95
+
96
+ #update the Hash only if the new with is at least equal to the old one
97
+ #due to arithmetic errors we need to ignore a small difference in the new and the old sum
98
+ #the same had to be done in the column_widht_calculator#natural_width
99
+ update_hash = ((cell.width.to_f - current_width_of_spanned_cells) >
100
+ Prawn::FLOAT_PRECISION)
101
+
102
+ if update_hash
103
+ # Split the width of colspanned cells evenly by columns
104
+ width_per_column = cell.width.to_f / cell.colspan
105
+ # Update the Hash
106
+ cell.colspan.times do |i|
107
+ @widths_by_column[cell.column + i] = width_per_column
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ @widths_by_column.sort_by { |col, _| col }.map { |_, w| w }
114
+ end
115
+
116
+ # get column widths (either min or max depending on meth)
117
+ # used in cells.rb
118
+ #
119
+ # @param row_or_column - you may call this on either rows or columns
120
+ # @param meth - min/max
121
+ # @param aggregate - functions from cell.rb to be used to aggregate e.g. avg_spanned_min_width
122
+ #
123
+ def aggregate_cell_values(row_or_column, meth, aggregate)
124
+ values = {}
125
+
126
+ #calculate values for all cells that do not span accross multiple cells
127
+ #this ensures that we don't have a problem if the first line includes
128
+ #a cell that spans across multiple cells
129
+ @cells.each do |cell|
130
+ #don't take spanned cells
131
+ if cell.colspan == 1 and cell.class != Prawn::Table::Cell::SpanDummy
132
+ index = cell.send(row_or_column)
133
+ values[index] = [values[index], cell.send(meth)].compact.send(aggregate)
134
+ end
135
+ end
136
+
137
+ # if there are only colspanned or rowspanned cells in a table
138
+ spanned_width_needs_fixing = true
139
+
140
+ @cells.each do |cell|
141
+ index = cell.send(row_or_column)
142
+ if cell.colspan > 1
143
+ #special treatment if some but not all spanned indices in the values array have been calculated
144
+ #only applies to rows
145
+ values = fill_values_if_needed(values, cell, index, meth) if row_or_column == :column
146
+ #calculate current (old) return value before we do anything
147
+ old_sum = 0
148
+ cell.colspan.times { |i|
149
+ old_sum += values[index+i] unless values[index+i].nil?
150
+ }
151
+
152
+ #calculate future return value
153
+ new_sum = cell.send(meth) * cell.colspan
154
+
155
+ #due to float rounding errors we need to ignore a small difference in the new
156
+ #and the old sum the same had to be done in
157
+ #the column_width_calculator#natural_width
158
+ spanned_width_needs_fixing = ((new_sum - old_sum) > Prawn::FLOAT_PRECISION)
159
+
160
+ if spanned_width_needs_fixing
161
+ #not entirely sure why we need this line, but with it the tests pass
162
+ values[index] = [values[index], cell.send(meth)].compact.send(aggregate)
163
+ #overwrite the old values with the new ones, but only if all entries existed
164
+ entries_exist = true
165
+ cell.colspan.times { |i| entries_exist = false if values[index+i].nil? }
166
+ cell.colspan.times { |i|
167
+ values[index+i] = cell.send(meth) if entries_exist
168
+ }
169
+ end
170
+ else
171
+ if spanned_width_needs_fixing && cell.class == Prawn::Table::Cell::SpanDummy
172
+ values[index] = [values[index], cell.send(meth)].compact.send(aggregate)
173
+ end
174
+ end
175
+ end
176
+
177
+ return values.values.inject(0, &:+)
178
+ end
179
+ end
180
+
181
+ end
182
+ end
@@ -0,0 +1,5 @@
1
+ module Prawn
2
+ class Table
3
+ VERSION = '1.0.0.rc1'.freeze
4
+ end
5
+ end