prawn-table-continued 1.0.0.rc1

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