prawn-table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) 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 +5 -0
  6. data/LICENSE +56 -0
  7. data/lib/prawn/table.rb +641 -0
  8. data/lib/prawn/table/cell.rb +772 -0
  9. data/lib/prawn/table/cell/image.rb +69 -0
  10. data/lib/prawn/table/cell/in_table.rb +33 -0
  11. data/lib/prawn/table/cell/span_dummy.rb +93 -0
  12. data/lib/prawn/table/cell/subtable.rb +66 -0
  13. data/lib/prawn/table/cell/text.rb +154 -0
  14. data/lib/prawn/table/cells.rb +255 -0
  15. data/lib/prawn/table/column_width_calculator.rb +182 -0
  16. data/manual/contents.rb +13 -0
  17. data/manual/example_helper.rb +8 -0
  18. data/manual/table/basic_block.rb +53 -0
  19. data/manual/table/before_rendering_page.rb +26 -0
  20. data/manual/table/cell_border_lines.rb +24 -0
  21. data/manual/table/cell_borders_and_bg.rb +31 -0
  22. data/manual/table/cell_dimensions.rb +30 -0
  23. data/manual/table/cell_text.rb +38 -0
  24. data/manual/table/column_widths.rb +30 -0
  25. data/manual/table/content_and_subtables.rb +39 -0
  26. data/manual/table/creation.rb +27 -0
  27. data/manual/table/filtering.rb +36 -0
  28. data/manual/table/flow_and_header.rb +17 -0
  29. data/manual/table/image_cells.rb +33 -0
  30. data/manual/table/position.rb +29 -0
  31. data/manual/table/row_colors.rb +20 -0
  32. data/manual/table/span.rb +30 -0
  33. data/manual/table/style.rb +22 -0
  34. data/manual/table/table.rb +52 -0
  35. data/manual/table/width.rb +27 -0
  36. data/prawn-table.gemspec +48 -0
  37. data/spec/cell_spec.rb +629 -0
  38. data/spec/extensions/encoding_helpers.rb +11 -0
  39. data/spec/extensions/mocha.rb +46 -0
  40. data/spec/spec_helper.rb +53 -0
  41. data/spec/table/span_dummy_spec.rb +17 -0
  42. data/spec/table_spec.rb +1527 -0
  43. metadata +240 -0
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module EncodingHelpers
4
+ def win1252_string(str)
5
+ str.force_encoding(Encoding::Windows_1252)
6
+ end
7
+
8
+ def bin_string(str)
9
+ str.force_encoding(Encoding::ASCII_8BIT)
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ # Allow speccing things when an expectation matcher runs. Similar to #with, but
4
+ # always succeeds.
5
+ #
6
+ # @pdf.expects(:stroke_line).checking do |from, to|
7
+ # @pdf.map_to_absolute(from).should == [0, 0]
8
+ # end
9
+ #
10
+ # Note that the outer expectation does *not* fail only because the inner one
11
+ # does; in the above example, the outer expectation would only fail if
12
+ # stroke_line were not called.
13
+
14
+ class ParameterChecker < Mocha::ParametersMatcher
15
+ def initialize(&matching_block)
16
+ @expected_parameters = [Mocha::ParameterMatchers::AnyParameters.new]
17
+ @matching_block = matching_block
18
+ end
19
+
20
+ def match?(actual_parameters = [])
21
+ @matching_block.call(*actual_parameters)
22
+
23
+ true # always succeed
24
+ end
25
+ end
26
+
27
+ class Mocha::Expectation
28
+ def checking(&block)
29
+ @parameters_matcher = ParameterChecker.new(&block)
30
+ self
31
+ end
32
+ end
33
+
34
+
35
+ # Equivalent to expects(method_name).at_least(0). More useful when combined
36
+ # with parameter matchers to ignore certain calls for the sake of parameter
37
+ # matching.
38
+ #
39
+ # @pdf.ignores(:stroke_color=).with("000000")
40
+ # @pdf.expects(:stroke_color=).with("ff0000")
41
+ #
42
+ module Mocha::ObjectMethods
43
+ def ignores(method_name)
44
+ expects(method_name).at_least(0)
45
+ end
46
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ puts "Prawn specs: Running on Ruby Version: #{RUBY_VERSION}"
4
+
5
+ require "bundler"
6
+ Bundler.setup
7
+
8
+ if ENV["COVERAGE"]
9
+ require "simplecov"
10
+ SimpleCov.start do
11
+ add_filter "/spec/"
12
+ end
13
+ end
14
+
15
+ require "prawn"
16
+ require_relative "../lib/prawn/table"
17
+
18
+ Prawn.debug = true
19
+
20
+ require "rspec"
21
+ require "mocha/api"
22
+ require "pdf/reader"
23
+ require "pdf/inspector"
24
+
25
+ # Requires supporting ruby files with custom matchers and macros, etc,
26
+ # in spec/extensions/ and its subdirectories.
27
+ Dir[File.dirname(__FILE__) + "/extensions/**/*.rb"].each {|f| require f }
28
+
29
+ RSpec.configure do |config|
30
+ config.mock_framework = :mocha
31
+ config.include EncodingHelpers
32
+ config.treat_symbols_as_metadata_keys_with_true_values = true
33
+ end
34
+
35
+ def create_pdf(klass=Prawn::Document)
36
+ @pdf = klass.new(:margin => 0)
37
+ end
38
+
39
+ RSpec::Matchers.define :have_parseable_xobjects do
40
+ match do |actual|
41
+ expect { PDF::Inspector::XObject.analyze(actual.render) }.not_to raise_error
42
+ true
43
+ end
44
+ failure_message_for_should do |actual|
45
+ "expected that #{actual}'s XObjects could be successfully parsed"
46
+ end
47
+ end
48
+
49
+ # Make some methods public to assist in testing
50
+ module Prawn::Graphics
51
+ public :map_to_absolute
52
+ end
53
+
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), "..", "spec_helper")
4
+ require 'set'
5
+
6
+ describe "Prawn::Table::Cell::SpanDummy" do
7
+ before(:each) do
8
+ @pdf = Prawn::Document.new
9
+ @table = @pdf.table([[{:content => "Row", :colspan => 2}]])
10
+ @master_cell = @table.cells[0,0]
11
+ @span_dummy = @master_cell.dummy_cells.first
12
+ end
13
+
14
+ it "delegates background_color to the master cell" do
15
+ @span_dummy.background_color.should == @master_cell.background_color
16
+ end
17
+ end
@@ -0,0 +1,1527 @@
1
+ # encoding: utf-8
2
+
3
+ # run rspec -t issue:XYZ to run tests for a specific github issue
4
+ # or rspec -t unresolved to run tests for all unresolved issues
5
+
6
+
7
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
8
+
9
+ require_relative "../lib/prawn/table"
10
+ require 'set'
11
+
12
+ describe "Prawn::Table" do
13
+
14
+ describe "converting data to Cell objects" do
15
+ before(:each) do
16
+ @pdf = Prawn::Document.new
17
+ @table = @pdf.table([%w[R0C0 R0C1], %w[R1C0 R1C1]])
18
+ end
19
+
20
+ it "should return a Prawn::Table" do
21
+ @table.should be_a_kind_of Prawn::Table
22
+ end
23
+
24
+ it "should flatten the data into the @cells array in row-major order" do
25
+ @table.cells.map { |c| c.content }.should == %w[R0C0 R0C1 R1C0 R1C1]
26
+ end
27
+
28
+ it "should add row and column numbers to each cell" do
29
+ c = @table.cells.to_a.first
30
+ c.row.should == 0
31
+ c.column.should == 0
32
+ end
33
+
34
+ it "should allow empty fields" do
35
+ lambda {
36
+ data = [["foo","bar"],["baz",""]]
37
+ @pdf.table(data)
38
+ }.should_not raise_error
39
+ end
40
+
41
+ it "should allow a table with a header but no body" do
42
+ lambda { @pdf.table([["Header"]], :header => true) }.should_not raise_error
43
+ end
44
+
45
+ it "should accurately count columns from data" do
46
+ # First data row may contain colspan which would hide true column count
47
+ data = [["Name:", {:content => "Some very long name", :colspan => 5}]]
48
+ pdf = Prawn::Document.new
49
+ table = Prawn::Table.new data, pdf
50
+ table.column_widths.length.should == 6
51
+ end
52
+ end
53
+
54
+ describe "headers should allow for rowspan" do
55
+ it "should remember rowspans accross multiple pages", :issue => 721 do
56
+ pdf = Prawn::Document.new({:page_size => "A4", :page_layout => :portrait})
57
+ rows = [ [{:content=>"The\nNumber", :rowspan=>2}, {:content=>"Prefixed", :colspan=>2} ],
58
+ ["A's", "B's"] ]
59
+
60
+ (1..50).each do |n|
61
+ rows.push( ["#{n}", "A#{n}", "B#{n}"] )
62
+ end
63
+
64
+ pdf.table( rows, :header=>2 ) do
65
+ row(0..1).style :background_color=>"FFFFCC"
66
+ end
67
+
68
+ #ensure that the header on page 1 is identical to the header on page 0
69
+ output = PDF::Inspector::Page.analyze(pdf.render)
70
+ output.pages[0][:strings][0..4].should == output.pages[1][:strings][0..4]
71
+ end
72
+ end
73
+
74
+ describe "Text may be longer than the available space in a row on a single page" do
75
+ it "should not glitch the layout if there is too much text to fit onto a single row on a single page", :unresolved, :issue => 562 do
76
+ pdf = Prawn::Document.new({:page_size => "A4", :page_layout => :portrait})
77
+
78
+ table_data = Array.new
79
+ text = 'This will be a very long text. ' * 5
80
+ table_data.push([{:content => text, :rowspan => 2}, 'b', 'c'])
81
+ table_data.push(['b','c'])
82
+
83
+ column_widths = [50, 60, 400]
84
+
85
+ table = Prawn::Table.new table_data, pdf,:column_widths => column_widths
86
+
87
+ #render the table onto the pdf
88
+ table.draw
89
+
90
+ #expected behavior would be for the long text to be cut off or an exception to be raised
91
+ #thus we only expect a single page
92
+ pdf.page_count.should == 1
93
+ end
94
+ end
95
+
96
+ describe "You can explicitly set the column widths and use a colspan > 1" do
97
+
98
+ it "should tolerate floating point rounding errors < 0.000000001" do
99
+ data=[["a", "b ", "c ", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
100
+ [{:content=>"Foobar", :colspan=>12}]
101
+ ]
102
+ #we need values with lots of decimals so that arithmetic errors will occur
103
+ #the values are not arbitrary but where found converting mm to pdf pt
104
+ column_widths=[137, 40, 40, 54.69291338582678, 54.69291338582678,
105
+ 54.69291338582678, 54.69291338582678, 54.69291338582678,
106
+ 54.69291338582678, 54.69291338582678, 54.69291338582678,
107
+ 54.69291338582678]
108
+
109
+ pdf = Prawn::Document.new({:page_size => 'A4', :page_layout => :landscape})
110
+ table = Prawn::Table.new data, pdf, :column_widths => column_widths
111
+ table.column_widths.should == column_widths
112
+ end
113
+
114
+ it "should work with two different given colspans", :issue => 628 do
115
+ data = [
116
+ [" ", " ", " "],
117
+ [{:content=>" ", :colspan=>3}],
118
+ [" ", {:content=>" ", :colspan=>2}]
119
+ ]
120
+ column_widths = [60, 240, 60]
121
+ pdf = Prawn::Document.new
122
+ #the next line raised an Prawn::Errors::CannotFit exception before issue 628 was fixed
123
+ table = Prawn::Table.new data, pdf, :column_widths => column_widths
124
+ table.column_widths.should == column_widths
125
+ end
126
+
127
+ it "should work with a colspan > 1 with given column_widths (issue #407)" do
128
+ #normal entries in line 1
129
+ data = [
130
+ [ '','',''],
131
+ [ { :content => "", :colspan => 3 } ],
132
+ [ "", "", "" ],
133
+ ]
134
+ pdf = Prawn::Document.new
135
+ table = Prawn::Table.new data, pdf, :column_widths => [100 , 200, 240]
136
+
137
+ #colspan entry in line 1
138
+ data = [
139
+ [ { :content => "", :colspan => 3 } ],
140
+ [ "", "", "" ],
141
+ ]
142
+ pdf = Prawn::Document.new
143
+ table = Prawn::Table.new data, pdf, :column_widths => [100 , 200, 240]
144
+
145
+ #mixed entries in line 1
146
+ data = [
147
+ [ { :content => "", :colspan =>2 }, "" ],
148
+ [ "", "", "" ],
149
+ ]
150
+ pdf = Prawn::Document.new
151
+ table = Prawn::Table.new data, pdf, :column_widths => [100 , 200, 240]
152
+
153
+ data = [['', '', {:content => '', :colspan => 2}, '',''],
154
+ ['',{:content => '', :colspan => 5}]
155
+ ]
156
+ pdf = Prawn::Document.new
157
+ table = Prawn::Table.new data, pdf, :column_widths => [50 , 100, 50, 50, 50, 50]
158
+
159
+ end
160
+
161
+ it "should not increase column width when rendering a subtable",
162
+ :unresolved, :issue => 612 do
163
+
164
+ pdf = Prawn::Document.new
165
+
166
+ first = {:content=>"Foooo fo foooooo",:width=>50,:align=>:center}
167
+ second = {:content=>"Foooo",:colspan=>2,:width=>70,:align=>:center}
168
+ third = {:content=>"fooooooooooo, fooooooooooooo, fooo, foooooo fooooo",:width=>50,:align=>:center}
169
+ fourth = {:content=>"Bar",:width=>20,:align=>:center}
170
+
171
+ table_content = [[
172
+ first,
173
+ [[second],[third,fourth]]
174
+ ]]
175
+
176
+ table = Prawn::Table.new table_content, pdf
177
+ table.column_widths.should == [50.0, 70.0]
178
+ end
179
+
180
+ it "illustrates issue #710", :issue => 710 do
181
+ partial_width = 40
182
+ pdf = Prawn::Document.new({page_size: "LETTER", page_layout: :portrait})
183
+ col_widths = [
184
+ 50,
185
+ partial_width, partial_width, partial_width, partial_width
186
+ ]
187
+
188
+ day_header = [{
189
+ content: "Monday, August 5th, A.S. XLIX",
190
+ colspan: 5,
191
+ }]
192
+
193
+ times = [{
194
+ content: "Loc",
195
+ colspan: 1,
196
+ }, {
197
+ content: "8:00",
198
+ colspan: 4,
199
+ }]
200
+
201
+ data = [ day_header ] + [ times ]
202
+
203
+ #raised a Prawn::Errors::CannotFit:
204
+ #Table's width was set larger than its contents' maximum width (max width 210, requested 218.0)
205
+ table = Prawn::Table.new data, pdf, :column_widths => col_widths
206
+ end
207
+
208
+ it "illustrate issue #533" do
209
+ data = [['', '', '', '', '',''],
210
+ ['',{:content => '', :colspan => 5}]]
211
+ pdf = Prawn::Document.new
212
+ table = Prawn::Table.new data, pdf, :column_widths => [50, 200, 40, 40, 50, 50]
213
+ end
214
+
215
+ it "illustrates issue #502" do
216
+ pdf = Prawn::Document.new
217
+ first = {:content=>"Foooo fo foooooo",:width=>50,:align=>:center}
218
+ second = {:content=>"Foooo",:colspan=>2,:width=>70,:align=>:center}
219
+ third = {:content=>"fooooooooooo, fooooooooooooo, fooo, foooooo fooooo",:width=>50,:align=>:center}
220
+ fourth = {:content=>"Bar",:width=>20,:align=>:center}
221
+ table_content = [[
222
+ first,
223
+ [[second],[third,fourth]]
224
+ ]]
225
+ pdf.move_down(20)
226
+ table = Prawn::Table.new table_content, pdf
227
+ pdf.table(table_content)
228
+ end
229
+
230
+ #https://github.com/prawnpdf/prawn/issues/407#issuecomment-28556698
231
+ it "correctly computes column widths with empty cells + colspan" do
232
+ data = [['', ''],
233
+ [{:content => '', :colspan => 2}]
234
+ ]
235
+ pdf = Prawn::Document.new
236
+
237
+ table = Prawn::Table.new data, pdf, :column_widths => [50, 200]
238
+ table.column_widths.should == [50.0, 200.0]
239
+ end
240
+
241
+ it "illustrates a variant of problem in issue #407 - comment 28556698" do
242
+ pdf = Prawn::Document.new
243
+ table_data = [["a", "b", "c"], [{:content=>"d", :colspan=>3}]]
244
+ column_widths = [50, 60, 400]
245
+
246
+ # Before we fixed #407, this line incorrectly raise a CannotFit error
247
+ pdf.table(table_data, :column_widths => column_widths)
248
+ end
249
+
250
+ it "should not allow oversized subtables when parent column width is constrained" do
251
+ pdf = Prawn::Document.new
252
+ child_1 = pdf.make_table([['foo'*100]])
253
+ child_2 = pdf.make_table([['foo']])
254
+ lambda do
255
+ pdf.table([[child_1], [child_2]], column_widths: [pdf.bounds.width/2] * 2)
256
+ end.should raise_error(Prawn::Errors::CannotFit)
257
+ end
258
+ end
259
+
260
+ describe "#initialize" do
261
+ before(:each) do
262
+ @pdf = Prawn::Document.new
263
+ end
264
+
265
+ it "should instance_eval a 0-arg block" do
266
+ initializer = mock()
267
+ initializer.expects(:kick).once
268
+
269
+ @pdf.table([["a"]]){
270
+ initializer.kick
271
+ }
272
+ end
273
+
274
+ it "should call a 1-arg block with the document as the argument" do
275
+ initializer = mock()
276
+ initializer.expects(:kick).once
277
+
278
+ @pdf.table([["a"]]){ |doc|
279
+ doc.should be_a_kind_of(Prawn::Table); initializer.kick }
280
+ end
281
+
282
+ it "should proxy cell methods to #cells" do
283
+ table = @pdf.table([["a"]], :cell_style => { :padding => 11 })
284
+ table.cells[0, 0].padding.should == [11, 11, 11, 11]
285
+ end
286
+
287
+ it "should set row and column length" do
288
+ table = @pdf.table([["a", "b", "c"], ["d", "e", "f"]])
289
+ table.row_length.should == 2
290
+ table.column_length.should == 3
291
+ end
292
+
293
+ it "should generate a text cell based on a String" do
294
+ t = @pdf.table([["foo"]])
295
+ t.cells[0,0].should be_a_kind_of(Prawn::Table::Cell::Text)
296
+ end
297
+
298
+ it "should pass through a text cell" do
299
+ c = Prawn::Table::Cell::Text.new(@pdf, [0,0], :content => "foo")
300
+ t = @pdf.table([[c]])
301
+ t.cells[0,0].should == c
302
+ end
303
+ end
304
+
305
+ describe "cell accessors" do
306
+ before(:each) do
307
+ @pdf = Prawn::Document.new
308
+ @table = @pdf.table([%w[R0C0 R0C1], %w[R1C0 R1C1]])
309
+ end
310
+
311
+ it "should select rows by number or range" do
312
+ Set.new(@table.row(0).map { |c| c.content }).should ==
313
+ Set.new(%w[R0C0 R0C1])
314
+ Set.new(@table.rows(0..1).map { |c| c.content }).should ==
315
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
316
+ end
317
+
318
+ it "should select rows by array" do
319
+ Set.new(@table.rows([0, 1]).map { |c| c.content }).should ==
320
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
321
+ end
322
+
323
+ it "should allow negative row selectors" do
324
+ Set.new(@table.row(-1).map { |c| c.content }).should ==
325
+ Set.new(%w[R1C0 R1C1])
326
+ Set.new(@table.rows(-2..-1).map { |c| c.content }).should ==
327
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
328
+ Set.new(@table.rows(0..-1).map { |c| c.content }).should ==
329
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
330
+ end
331
+
332
+ it "should select columns by number or range" do
333
+ Set.new(@table.column(0).map { |c| c.content }).should ==
334
+ Set.new(%w[R0C0 R1C0])
335
+ Set.new(@table.columns(0..1).map { |c| c.content }).should ==
336
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
337
+ end
338
+
339
+ it "should select columns by array" do
340
+ Set.new(@table.columns([0, 1]).map { |c| c.content }).should ==
341
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
342
+ end
343
+
344
+ it "should allow negative column selectors" do
345
+ Set.new(@table.column(-1).map { |c| c.content }).should ==
346
+ Set.new(%w[R0C1 R1C1])
347
+ Set.new(@table.columns(-2..-1).map { |c| c.content }).should ==
348
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
349
+ Set.new(@table.columns(0..-1).map { |c| c.content }).should ==
350
+ Set.new(%w[R0C0 R0C1 R1C0 R1C1])
351
+ end
352
+
353
+ it "should allow rows and columns to be combined" do
354
+ @table.row(0).column(1).map { |c| c.content }.should == ["R0C1"]
355
+ end
356
+
357
+ it "should accept a filter block, returning a cell proxy" do
358
+ @table.cells.filter { |c| c.content =~ /R0/ }.column(1).map{ |c|
359
+ c.content }.should == ["R0C1"]
360
+ end
361
+
362
+ it "should accept the [] method, returning a Cell or nil" do
363
+ @table.cells[0, 0].content.should == "R0C0"
364
+ @table.cells[12, 12].should be_nil
365
+ end
366
+
367
+ it "should proxy unknown methods to the cells" do
368
+ @table.cells.height = 200
369
+ @table.row(1).height = 100
370
+
371
+ @table.cells[0, 0].height.should == 200
372
+ @table.cells[1, 0].height.should == 100
373
+ end
374
+
375
+ it "should ignore non-setter methods" do
376
+ lambda {
377
+ @table.cells.content_width
378
+ }.should raise_error(NoMethodError)
379
+ end
380
+
381
+ it "skips cells that don't respond to the given method" do
382
+ table = @pdf.make_table([[{:content => "R0", :colspan => 2}],
383
+ %w[R1C0 R1C1]])
384
+ lambda {
385
+ table.row(0).font_style = :bold
386
+ }.should_not raise_error
387
+ end
388
+
389
+ it "should accept the style method, proxying its calls to the cells" do
390
+ @table.cells.style(:height => 200, :width => 200)
391
+ @table.column(0).style(:width => 100)
392
+
393
+ @table.cells[0, 1].width.should == 200
394
+ @table.cells[1, 0].height.should == 200
395
+ @table.cells[1, 0].width.should == 100
396
+ end
397
+
398
+ it "style method should accept a block, passing each cell to be styled" do
399
+ @table.cells.style { |c| c.height = 200 }
400
+ @table.cells[0, 1].height.should == 200
401
+ end
402
+
403
+ it "should return the width of selected columns for #width" do
404
+ c0_width = @table.column(0).map{ |c| c.width }.max
405
+ c1_width = @table.column(1).map{ |c| c.width }.max
406
+
407
+ @table.column(0).width.should == c0_width
408
+ @table.column(1).width.should == c1_width
409
+
410
+ @table.columns(0..1).width.should == c0_width + c1_width
411
+ @table.cells.width.should == c0_width + c1_width
412
+ end
413
+
414
+ it "should return the height of selected rows for #height" do
415
+ r0_height = @table.row(0).map{ |c| c.height }.max
416
+ r1_height = @table.row(1).map{ |c| c.height }.max
417
+
418
+ @table.row(0).height.should == r0_height
419
+ @table.row(1).height.should == r1_height
420
+
421
+ @table.rows(0..1).height.should == r0_height + r1_height
422
+ @table.cells.height.should == r0_height + r1_height
423
+ end
424
+ end
425
+
426
+ describe "layout" do
427
+ before(:each) do
428
+ @pdf = Prawn::Document.new
429
+ @long_text = "The quick brown fox jumped over the lazy dogs. " * 5
430
+ end
431
+
432
+ describe "width" do
433
+ it "should raise_error an error if the given width is outside of range" do
434
+ lambda do
435
+ @pdf.table([["foo"]], :width => 1)
436
+ end.should raise_error(Prawn::Errors::CannotFit)
437
+
438
+ lambda do
439
+ @pdf.table([[@long_text]], :width => @pdf.bounds.width + 100)
440
+ end.should raise_error(Prawn::Errors::CannotFit)
441
+ end
442
+
443
+ it "should accept the natural width for small tables" do
444
+ pad = 10 # default padding
445
+ @table = @pdf.table([["a"]])
446
+ @table.width.should == @table.cells[0, 0].natural_content_width + pad
447
+ end
448
+
449
+ it "width should == sum(column_widths)" do
450
+ table = Prawn::Table.new([%w[ a b c ], %w[d e f]], @pdf) do
451
+ column(0).width = 50
452
+ column(1).width = 100
453
+ column(2).width = 150
454
+ end
455
+ table.width.should == 300
456
+ end
457
+
458
+ it "should accept Numeric for column_widths" do
459
+ table = Prawn::Table.new([%w[ a b c ], %w[d e f]], @pdf) do |t|
460
+ t.column_widths = 50
461
+ end
462
+ table.width.should == 150
463
+ end
464
+
465
+ it "should calculate unspecified column widths as "+
466
+ "(max(string_width) + 2*horizontal_padding)" do
467
+ hpad, fs = 3, 12
468
+ columns = 2
469
+ table = Prawn::Table.new( [%w[ foo b ], %w[d foobar]], @pdf,
470
+ :cell_style => { :padding => hpad, :size => fs } )
471
+
472
+ col0_width = @pdf.width_of("foo", :size => fs)
473
+ col1_width = @pdf.width_of("foobar", :size => fs)
474
+
475
+ table.width.should == col0_width + col1_width + 2*columns*hpad
476
+ end
477
+
478
+ it "should allow mixing autocalculated and preset"+
479
+ "column widths within a single table" do
480
+ hpad, fs = 10, 6
481
+ stretchy_columns = 2
482
+
483
+ col0_width = 50
484
+ col1_width = @pdf.width_of("foo", :size => fs)
485
+ col2_width = @pdf.width_of("foobar", :size => fs)
486
+ col3_width = 150
487
+
488
+ table = Prawn::Table.new( [%w[snake foo b apple],
489
+ %w[kitten d foobar banana]], @pdf,
490
+ :cell_style => { :padding => hpad, :size => fs }) do
491
+
492
+ column(0).width = col0_width
493
+ column(3).width = col3_width
494
+ end
495
+
496
+ table.width.should == col1_width + col2_width +
497
+ 2*stretchy_columns*hpad +
498
+ col0_width + col3_width
499
+ end
500
+
501
+ it "should preserve all manually requested column widths" do
502
+ col0_width = 50
503
+ col1_width = 20
504
+ col3_width = 60
505
+
506
+ table = Prawn::Table.new( [["snake", "foo", "b",
507
+ "some long, long text that will wrap"],
508
+ %w[kitten d foobar banana]], @pdf,
509
+ :width => 150) do
510
+
511
+ column(0).width = col0_width
512
+ column(1).width = col1_width
513
+ column(3).width = col3_width
514
+ end
515
+
516
+ table.draw
517
+
518
+ table.column(0).width.should == col0_width
519
+ table.column(1).width.should == col1_width
520
+ table.column(3).width.should == col3_width
521
+ end
522
+
523
+ it "should_not exceed the maximum width of the margin_box" do
524
+ expected_width = @pdf.margin_box.width
525
+ data = [
526
+ ['This is a column with a lot of text that should comfortably exceed '+
527
+ 'the width of a normal document margin_box width', 'Some more text',
528
+ 'and then some more', 'Just a bit more to be extra sure']
529
+ ]
530
+ table = Prawn::Table.new(data, @pdf)
531
+
532
+ table.width.should == expected_width
533
+ end
534
+
535
+ it "should_not exceed the maximum width of the margin_box even with" +
536
+ "manual widths specified" do
537
+ expected_width = @pdf.margin_box.width
538
+ data = [
539
+ ['This is a column with a lot of text that should comfortably exceed '+
540
+ 'the width of a normal document margin_box width', 'Some more text',
541
+ 'and then some more', 'Just a bit more to be extra sure']
542
+ ]
543
+ table = Prawn::Table.new(data, @pdf) { column(1).width = 100 }
544
+
545
+ table.width.should == expected_width
546
+ end
547
+
548
+ it "scales down only the non-preset column widths when the natural width" +
549
+ "exceeds the maximum width of the margin_box" do
550
+ expected_width = @pdf.margin_box.width
551
+ data = [
552
+ ['This is a column with a lot of text that should comfortably exceed '+
553
+ 'the width of a normal document margin_box width', 'Some more text',
554
+ 'and then some more', 'Just a bit more to be extra sure']
555
+ ]
556
+ table = Prawn::Table.new(data, @pdf) { column(1).width = 100; column(3).width = 50 }
557
+
558
+ table.width.should == expected_width
559
+ table.column_widths[1].should == 100
560
+ table.column_widths[3].should == 50
561
+ end
562
+
563
+ it "should allow width to be reset even after it has been calculated" do
564
+ @table = @pdf.table([[@long_text]])
565
+ @table.width
566
+ @table.width = 100
567
+ @table.width.should == 100
568
+ end
569
+
570
+ it "should shrink columns evenly when two equal columns compete" do
571
+ @table = @pdf.table([["foo", @long_text], [@long_text, "foo"]])
572
+ @table.cells[0, 0].width.should == @table.cells[0, 1].width
573
+ end
574
+
575
+ it "should grow columns evenly when equal deficient columns compete" do
576
+ @table = @pdf.table([["foo", "foobar"], ["foobar", "foo"]], :width => 500)
577
+ @table.cells[0, 0].width.should == @table.cells[0, 1].width
578
+ end
579
+
580
+ it "should respect manual widths" do
581
+ @table = @pdf.table([%w[foo bar baz], %w[baz bar foo]], :width => 500) do
582
+ column(1).width = 60
583
+ end
584
+ @table.column(1).width.should == 60
585
+ @table.column(0).width.should == @table.column(2).width
586
+ end
587
+
588
+ it "should allow table cells to be resized in block" do
589
+ # if anything goes wrong, a CannotFit error will be raised
590
+
591
+ @pdf.table([%w[1 2 3 4 5]]) do |t|
592
+ t.width = 40
593
+ t.cells.size = 8
594
+ t.cells.padding = 0
595
+ end
596
+ end
597
+
598
+ it "should be the width of the :width parameter" do
599
+ expected_width = 300
600
+ table = Prawn::Table.new( [%w[snake foo b apple],
601
+ %w[kitten d foobar banana]], @pdf,
602
+ :width => expected_width)
603
+
604
+ table.width.should == expected_width
605
+ end
606
+
607
+ it "should_not exceed the :width option" do
608
+ expected_width = 400
609
+ data = [
610
+ ['This is a column with a lot of text that should comfortably exceed '+
611
+ 'the width of a normal document margin_box width', 'Some more text',
612
+ 'and then some more', 'Just a bit more to be extra sure']
613
+ ]
614
+ table = Prawn::Table.new(data, @pdf, :width => expected_width)
615
+
616
+ table.width.should == expected_width
617
+ end
618
+
619
+ it "should_not exceed the :width option even with manual widths specified" do
620
+ expected_width = 400
621
+ data = [
622
+ ['This is a column with a lot of text that should comfortably exceed '+
623
+ 'the width of a normal document margin_box width', 'Some more text',
624
+ 'and then some more', 'Just a bit more to be extra sure']
625
+ ]
626
+ table = Prawn::Table.new(data, @pdf, :width => expected_width) do
627
+ column(1).width = 100
628
+ end
629
+
630
+ table.width.should == expected_width
631
+ end
632
+
633
+ it "should calculate unspecified column widths even " +
634
+ "with colspan cells declared" do
635
+ pdf = Prawn::Document.new
636
+ hpad, fs = 3, 5
637
+ columns = 3
638
+
639
+ data = [ [ { :content => 'foo', :colspan => 2 }, "foobar" ],
640
+ [ "foo", "foo", "foo" ] ]
641
+ table = Prawn::Table.new( data, pdf,
642
+ :cell_style => {
643
+ :padding_left => hpad, :padding_right => hpad,
644
+ :size => fs
645
+ })
646
+
647
+ col0_width = pdf.width_of("foo", :size => fs) # cell 1, 0
648
+ col1_width = pdf.width_of("foo", :size => fs) # cell 1, 1
649
+ col2_width = pdf.width_of("foobar", :size => fs) # cell 0, 1 (at col 2)
650
+
651
+ table.width.should == col0_width + col1_width +
652
+ col2_width + 2*columns*hpad
653
+ end
654
+ end
655
+
656
+ describe "height" do
657
+ it "should set all cells in a row to the same height" do
658
+ @table = @pdf.table([["foo", @long_text]])
659
+ @table.cells[0, 0].height.should == @table.cells[0, 1].height
660
+ end
661
+
662
+ it "should move y-position to the bottom of the table after drawing" do
663
+ old_y = @pdf.y
664
+ table = @pdf.table([["foo"]])
665
+ @pdf.y.should == old_y - table.height
666
+ end
667
+
668
+ it "should_not wrap unnecessarily" do
669
+ # Test for FP errors and glitches
670
+ t = @pdf.table([["Bender Bending Rodriguez"]])
671
+ h = @pdf.height_of("one line")
672
+ (t.height - 10).should be < h*1.5
673
+ end
674
+
675
+ it "should have a height of n rows" do
676
+ data = [["foo"],["bar"],["baaaz"]]
677
+
678
+ vpad = 4
679
+ origin = @pdf.y
680
+ @pdf.table data, :cell_style => { :padding => vpad }
681
+
682
+ table_height = origin - @pdf.y
683
+ font_height = @pdf.font.height
684
+ line_gap = @pdf.font.line_gap
685
+
686
+ num_rows = data.length
687
+ table_height.should be_within(0.001).of(
688
+ num_rows * font_height + 2*vpad*num_rows )
689
+ end
690
+
691
+ end
692
+
693
+ describe "position" do
694
+ it "should center tables with :position => :center" do
695
+ @pdf.expects(:bounding_box).with do |(x, y), opts|
696
+ expected = (@pdf.bounds.width - 500) / 2.0
697
+ (x - expected).abs < 0.001
698
+ end
699
+
700
+ @pdf.table([["foo"]], :column_widths => 500, :position => :center)
701
+ end
702
+
703
+ it "should right-align tables with :position => :right" do
704
+ @pdf.expects(:bounding_box).with do |(x, y), opts|
705
+ expected = @pdf.bounds.width - 500
706
+ (x - expected).abs < 0.001
707
+ end
708
+
709
+ @pdf.table([["foo"]], :column_widths => 500, :position => :right)
710
+ end
711
+
712
+ it "should accept a Numeric" do
713
+ @pdf.expects(:bounding_box).with do |(x, y), opts|
714
+ expected = 123
715
+ (x - expected).abs < 0.001
716
+ end
717
+
718
+ @pdf.table([["foo"]], :column_widths => 500, :position => 123)
719
+ end
720
+
721
+ it "should raise_error an ArgumentError on unknown :position" do
722
+ lambda do
723
+ @pdf.table([["foo"]], :position => :bratwurst)
724
+ end.should raise_error(ArgumentError)
725
+ end
726
+ end
727
+
728
+ end
729
+
730
+ describe "Multi-page tables" do
731
+ it "should flow to the next page when hitting the bottom of the bounds" do
732
+ Prawn::Document.new { table([["foo"]] * 30) }.page_count.should == 1
733
+ Prawn::Document.new { table([["foo"]] * 31) }.page_count.should == 2
734
+ Prawn::Document.new { table([["foo"]] * 31); table([["foo"]] * 35) }.
735
+ page_count.should == 3
736
+ end
737
+
738
+ it "should respect the containing bounds" do
739
+ Prawn::Document.new do
740
+ bounding_box([0, cursor], :width => bounds.width, :height => 72) do
741
+ table([["foo"]] * 4)
742
+ end
743
+ end.page_count.should == 2
744
+ end
745
+
746
+ it "should_not start a new page before finishing out a row" do
747
+ Prawn::Document.new do
748
+ table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
749
+ end.page_count.should == 1
750
+ end
751
+
752
+ it "should only start new page on long cells if it would gain us height" do
753
+ Prawn::Document.new do
754
+ text "Hello"
755
+ table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
756
+ end.page_count.should == 2
757
+ end
758
+
759
+ it "should_not start a new page to gain height when at the top of " +
760
+ "a bounding box, even if stretchy" do
761
+ Prawn::Document.new do
762
+ bounding_box([bounds.left, bounds.top - 20], :width => 400) do
763
+ table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
764
+ end
765
+ end.page_count.should == 1
766
+ end
767
+
768
+ it "should still break to the next page if in a stretchy bounding box " +
769
+ "but not at the top" do
770
+ Prawn::Document.new do
771
+ bounding_box([bounds.left, bounds.top - 20], :width => 400) do
772
+ text "Hello"
773
+ table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
774
+ end
775
+ end.page_count.should == 2
776
+ end
777
+
778
+ it "should only draw first-page header if the first body row fits" do
779
+ pdf = Prawn::Document.new
780
+
781
+ pdf.y = 60 # not enough room for a table row
782
+ pdf.table [["Header"], ["Body"]], :header => true
783
+
784
+ output = PDF::Inspector::Page.analyze(pdf.render)
785
+ # Ensure we only drew the header once, on the second page
786
+ output.pages[0][:strings].should be_empty
787
+ output.pages[1][:strings].should == ["Header", "Body"]
788
+ end
789
+
790
+ it 'should only draw first-page header if the first multi-row fits',
791
+ :issue => 707 do
792
+ pdf = Prawn::Document.new
793
+
794
+ pdf.y = 100 # not enough room for the header and multirow cell
795
+ pdf.table [
796
+ [{content: 'Header', colspan: 2}],
797
+ [{content: 'Multirow cell', rowspan: 3}, 'Line 1'],
798
+ ] + (2..3).map { |i| ["Line #{i}"] }, :header => true
799
+
800
+ output = PDF::Inspector::Page.analyze(pdf.render)
801
+ # Ensure we only drew the header once, on the second page
802
+ output.pages[0][:strings].should == []
803
+ output.pages[1][:strings].should == ['Header', 'Multirow cell', 'Line 1',
804
+ 'Line 2', 'Line 3']
805
+ end
806
+
807
+ it "should draw background before borders, but only within pages" do
808
+ seq = sequence("drawing_order")
809
+
810
+ @pdf = Prawn::Document.new
811
+
812
+ # give enough room for only the first row
813
+ @pdf.y = @pdf.bounds.absolute_bottom + 30
814
+ t = @pdf.make_table([["A", "B"],
815
+ ["C", "D"]],
816
+ :cell_style => {:background_color => 'ff0000'})
817
+
818
+ ca = t.cells[0, 0]
819
+ cb = t.cells[0, 1]
820
+ cc = t.cells[1, 0]
821
+ cd = t.cells[1, 1]
822
+
823
+ # All backgrounds should draw before any borders on page 1...
824
+ ca.expects(:draw_background).in_sequence(seq)
825
+ cb.expects(:draw_background).in_sequence(seq)
826
+ ca.expects(:draw_borders).in_sequence(seq)
827
+ cb.expects(:draw_borders).in_sequence(seq)
828
+ # ...and page 2
829
+ @pdf.expects(:start_new_page).in_sequence(seq)
830
+ cc.expects(:draw_background).in_sequence(seq)
831
+ cd.expects(:draw_background).in_sequence(seq)
832
+ cc.expects(:draw_borders).in_sequence(seq)
833
+ cd.expects(:draw_borders).in_sequence(seq)
834
+
835
+ t.draw
836
+ end
837
+
838
+ describe "before_rendering_page callback" do
839
+ before(:each) { @pdf = Prawn::Document.new }
840
+
841
+ it "is passed all cells to be rendered on that page" do
842
+ kicked = 0
843
+
844
+ @pdf.table([["foo"]] * 100) do |t|
845
+ t.before_rendering_page do |page|
846
+ page.row_count.should == ((kicked < 3) ? 30 : 10)
847
+ page.column_count.should == 1
848
+ page.row(0).first.content.should == "foo"
849
+ page.row(-1).first.content.should == "foo"
850
+ kicked += 1
851
+ end
852
+ end
853
+
854
+ kicked.should == 4
855
+ end
856
+
857
+ it "numbers cells relative to their position on page" do
858
+ @pdf.table([["foo"]] * 100) do |t|
859
+ t.before_rendering_page do |page|
860
+ page[0, 0].content.should == "foo"
861
+ end
862
+ end
863
+ end
864
+
865
+ it "changing cells in the callback affects their rendering" do
866
+ seq = sequence("render order")
867
+
868
+ t = @pdf.make_table([["foo"]] * 40) do |table|
869
+ table.before_rendering_page do |page|
870
+ page[0, 0].background_color = "ff0000"
871
+ end
872
+ end
873
+
874
+ t.cells[30, 0].stubs(:draw_background).checking do |xy|
875
+ t.cells[30, 0].background_color.should == 'ff0000'
876
+ end
877
+ t.cells[31, 0].stubs(:draw_background).checking do |xy|
878
+ t.cells[31, 0].background_color.should == nil
879
+ end
880
+
881
+ t.draw
882
+ end
883
+
884
+ it "passes headers on page 2+" do
885
+ @pdf.table([["header"]] + [["foo"]] * 100, :header => true) do |t|
886
+ t.before_rendering_page do |page|
887
+ page[0, 0].content.should == "header"
888
+ end
889
+ end
890
+ end
891
+
892
+ it "updates dummy cell header rows" do
893
+ header = [[{:content => "header", :colspan => 2}]]
894
+ data = [["foo", "bar"]] * 31
895
+ @pdf.table(header + data, :header => true) do |t|
896
+ t.before_rendering_page do |page|
897
+ cell = page[0, 0]
898
+ cell.dummy_cells.each {|dc| dc.row.should == cell.row }
899
+ end
900
+ end
901
+ end
902
+
903
+ it "allows headers to be changed" do
904
+ seq = sequence("render order")
905
+ @pdf.expects(:draw_text!).with { |t, _| t == "hdr1"}.in_sequence(seq)
906
+ @pdf.expects(:draw_text!).with { |t, _| t == "foo"}.times(29).in_sequence(seq)
907
+ # Verify that the changed cell doesn't mutate subsequent pages
908
+ @pdf.expects(:draw_text!).with { |t, _| t == "header"}.in_sequence(seq)
909
+ @pdf.expects(:draw_text!).with { |t, _| t == "foo"}.times(11).in_sequence(seq)
910
+
911
+ set_first_page_headers = false
912
+ @pdf.table([["header"]] + [["foo"]] * 40, :header => true) do |t|
913
+ t.before_rendering_page do |page|
914
+ # only change first page header
915
+ page[0, 0].content = "hdr1" unless set_first_page_headers
916
+ set_first_page_headers = true
917
+ end
918
+ end
919
+ end
920
+ end
921
+ end
922
+
923
+ describe "#style" do
924
+ it "should send #style to its first argument, passing the style hash and" +
925
+ " block" do
926
+
927
+ stylable = stub()
928
+ stylable.expects(:style).with(:foo => :bar).once.yields
929
+
930
+ block = stub()
931
+ block.expects(:kick).once
932
+
933
+ Prawn::Document.new do
934
+ table([["x"]]) { style(stylable, :foo => :bar) { block.kick } }
935
+ end
936
+ end
937
+
938
+ it "should default to {} for the hash argument" do
939
+ stylable = stub()
940
+ stylable.expects(:style).with({}).once
941
+
942
+ Prawn::Document.new do
943
+ table([["x"]]) { style(stylable) }
944
+ end
945
+ end
946
+
947
+ it "ignores unknown values on a cell-by-cell basis" do
948
+ Prawn::Document.new do
949
+ table([["x", [["y"]]]], :cell_style => {:overflow => :shrink_to_fit})
950
+ end
951
+ end
952
+ end
953
+
954
+ describe "row_colors" do
955
+ it "should allow array syntax for :row_colors" do
956
+ data = [["foo"], ["bar"], ["baz"]]
957
+ pdf = Prawn::Document.new
958
+ t = pdf.table(data, :row_colors => ['cccccc', 'ffffff'])
959
+ t.cells.map{|x| x.background_color}.should == %w[cccccc ffffff cccccc]
960
+ end
961
+
962
+ it "should ignore headers" do
963
+ data = [["header"], ["foo"], ["bar"], ["baz"]]
964
+ pdf = Prawn::Document.new
965
+ t = pdf.table(data, :header => true,
966
+ :row_colors => ['cccccc', 'ffffff']) do
967
+ row(0).background_color = '333333'
968
+ end
969
+
970
+ t.cells.map{|x| x.background_color}.should ==
971
+ %w[333333 cccccc ffffff cccccc]
972
+ end
973
+
974
+ it "stripes rows consistently from page to page, skipping header rows" do
975
+ data = [["header"]] + [["foo"]] * 70
976
+ pdf = Prawn::Document.new
977
+ t = pdf.make_table(data, :header => true,
978
+ :row_colors => ['cccccc', 'ffffff']) do
979
+ cells.padding = 0
980
+ cells.size = 9
981
+ row(0).size = 11
982
+ end
983
+
984
+ # page 1: header + 67 cells (odd number -- verifies that the next
985
+ # page disrupts the even/odd coloring, since both the last data cell
986
+ # on this page and the first one on the next are colored cccccc)
987
+ Prawn::Table::Cell.expects(:draw_cells).with do |cells|
988
+ cells.map { |c, (x, y)| c.background_color } ==
989
+ [nil] + (%w[cccccc ffffff] * 33) + %w[cccccc]
990
+ end
991
+ # page 2: header and 3 data cells
992
+ Prawn::Table::Cell.expects(:draw_cells).with do |cells|
993
+ cells.map { |c, (x, y)| c.background_color } ==
994
+ [nil] + %w[cccccc ffffff cccccc]
995
+ end
996
+ t.draw
997
+ end
998
+
999
+ it "should_not override an explicit background_color" do
1000
+ data = [["foo"], ["bar"], ["baz"]]
1001
+ pdf = Prawn::Document.new
1002
+ table = pdf.table(data, :row_colors => ['cccccc', 'ffffff']) { |t|
1003
+ t.cells[0, 0].background_color = 'dddddd'
1004
+ }
1005
+ table.cells.map{|x| x.background_color}.should == %w[dddddd ffffff cccccc]
1006
+ end
1007
+ end
1008
+
1009
+ describe "inking" do
1010
+ before(:each) do
1011
+ @pdf = Prawn::Document.new
1012
+ end
1013
+
1014
+ it "should set the x-position of each cell based on widths" do
1015
+ @table = @pdf.table([["foo", "bar", "baz"]])
1016
+
1017
+ x = 0
1018
+ (0..2).each do |col|
1019
+ cell = @table.cells[0, col]
1020
+ cell.x.should == x
1021
+ x += cell.width
1022
+ end
1023
+ end
1024
+
1025
+ it "should set the y-position of each cell based on heights" do
1026
+ y = 0
1027
+ @table = @pdf.make_table([["foo"], ["bar"], ["baz"]])
1028
+
1029
+ (0..2).each do |row|
1030
+ cell = @table.cells[row, 0]
1031
+ cell.y.should be_within(0.01).of(y)
1032
+ y -= cell.height
1033
+ end
1034
+ end
1035
+
1036
+ it "should output content cell by cell, row by row" do
1037
+ data = [["foo","bar"],["baz","bang"]]
1038
+ @pdf = Prawn::Document.new
1039
+ @pdf.table(data)
1040
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1041
+ output.strings.should == data.flatten
1042
+ end
1043
+
1044
+ it "should_not cause an error if rendering the very first row causes a " +
1045
+ "page break" do
1046
+ Prawn::Document.new do |pdf|
1047
+ arr = Array(1..5).collect{|i| ["cell #{i}"] }
1048
+
1049
+ pdf.move_down( pdf.y - (pdf.bounds.absolute_bottom + 3) )
1050
+
1051
+ lambda {
1052
+ pdf.table(arr)
1053
+ }.should_not raise_error
1054
+ end
1055
+ end
1056
+
1057
+ it "should draw all backgrounds before any borders" do
1058
+ # lest backgrounds overlap borders:
1059
+ # https://github.com/sandal/prawn/pull/226
1060
+
1061
+ seq = sequence("drawing_order")
1062
+
1063
+ t = @pdf.make_table([["A", "B"]],
1064
+ :cell_style => {:background_color => 'ff0000'})
1065
+ ca = t.cells[0, 0]
1066
+ cb = t.cells[0, 1]
1067
+
1068
+ # XXX Not a perfectly general test, because it would still be acceptable
1069
+ # if we drew B then A
1070
+ ca.expects(:draw_background).in_sequence(seq)
1071
+ cb.expects(:draw_background).in_sequence(seq)
1072
+ ca.expects(:draw_borders).in_sequence(seq)
1073
+ cb.expects(:draw_borders).in_sequence(seq)
1074
+
1075
+ t.draw
1076
+ end
1077
+
1078
+ it "should allow multiple inkings of the same table" do
1079
+ pdf = Prawn::Document.new
1080
+ t = Prawn::Table.new([["foo"]], pdf)
1081
+
1082
+ pdf.expects(:bounding_box).with{|(x, y), options| y.to_i == 495}.yields
1083
+ pdf.expects(:bounding_box).with{|(x, y), options| y.to_i == 395}.yields
1084
+ pdf.expects(:draw_text!).with{ |text, options| text == 'foo' }.twice
1085
+
1086
+ pdf.move_cursor_to(500)
1087
+ t.draw
1088
+
1089
+ pdf.move_cursor_to(400)
1090
+ t.draw
1091
+ end
1092
+
1093
+ describe "in stretchy bounding boxes" do
1094
+ it "should draw all cells on a row at the same y-position" do
1095
+ pdf = Prawn::Document.new
1096
+
1097
+ text_y = pdf.y.to_i - 5 # text starts 5pt below current y pos (padding)
1098
+
1099
+ pdf.bounding_box([0, pdf.cursor], :width => pdf.bounds.width) do
1100
+ pdf.expects(:draw_text!).checking { |text, options|
1101
+ pdf.bounds.absolute_top.should == text_y
1102
+ }.times(3)
1103
+
1104
+ pdf.table([%w[a b c]])
1105
+ end
1106
+ end
1107
+ end
1108
+ end
1109
+
1110
+ describe "headers" do
1111
+ context "single row header" do
1112
+ it "should add headers to output when specified" do
1113
+ data = [["a", "b"], ["foo","bar"],["baz","bang"]]
1114
+ @pdf = Prawn::Document.new
1115
+ @pdf.table(data, :header => true)
1116
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1117
+ output.strings.should == data.flatten
1118
+ end
1119
+
1120
+ it "should repeat headers across pages" do
1121
+ data = [["foo","bar"]] * 30
1122
+ headers = ["baz","foobar"]
1123
+ @pdf = Prawn::Document.new
1124
+ @pdf.table([headers] + data, :header => true)
1125
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1126
+ output.strings.should == headers + data.flatten[0..-3] + headers +
1127
+ data.flatten[-2..-1]
1128
+ end
1129
+
1130
+ it "draws headers at the correct position" do
1131
+ data = [["header"]] + [["foo"]] * 40
1132
+
1133
+ Prawn::Table::Cell.expects(:draw_cells).times(2).checking do |cells|
1134
+ cells.each do |cell, pt|
1135
+ if cell.content == "header"
1136
+ # Assert that header text is drawn at the same location on each page
1137
+ if @header_location
1138
+ pt.should == @header_location
1139
+ else
1140
+ @header_location = pt
1141
+ end
1142
+ end
1143
+ end
1144
+ end
1145
+ @pdf = Prawn::Document.new
1146
+ @pdf.table(data, :header => true)
1147
+ end
1148
+
1149
+ it "draws headers at the correct position with column box" do
1150
+ data = [["header"]] + [["foo"]] * 40
1151
+
1152
+ Prawn::Table::Cell.expects(:draw_cells).times(2).checking do |cells|
1153
+ cells.each do |cell, pt|
1154
+ if cell.content == "header"
1155
+ pt[0].should == @pdf.bounds.left
1156
+ end
1157
+ end
1158
+ end
1159
+ @pdf = Prawn::Document.new
1160
+ @pdf.column_box [0, @pdf.cursor], :width => @pdf.bounds.width, :columns => 2 do
1161
+ @pdf.table(data, :header => true)
1162
+ end
1163
+ end
1164
+
1165
+ it "should_not draw header twice when starting new page" do
1166
+ @pdf = Prawn::Document.new
1167
+ @pdf.y = 0
1168
+ @pdf.table([["Header"], ["Body"]], :header => true)
1169
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1170
+ output.strings.should == ["Header", "Body"]
1171
+ end
1172
+ end
1173
+
1174
+ context "multiple row header" do
1175
+ it "should add headers to output when specified" do
1176
+ data = [["a", "b"], ["c", "d"], ["foo","bar"],["baz","bang"]]
1177
+ @pdf = Prawn::Document.new
1178
+ @pdf.table(data, :header => 2)
1179
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1180
+ output.strings.should == data.flatten
1181
+ end
1182
+
1183
+ it "should repeat headers across pages" do
1184
+ data = [["foo","bar"]] * 30
1185
+ headers = ["baz","foobar"] + ["bas", "foobaz"]
1186
+ @pdf = Prawn::Document.new
1187
+ @pdf.table([headers] + data, :header => 2)
1188
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1189
+ output.strings.should == headers + data.flatten[0..-3] + headers +
1190
+ data.flatten[-4..-1]
1191
+ end
1192
+
1193
+ it "draws headers at the correct position" do
1194
+ data = [["header"]] + [["header2"]] + [["foo"]] * 40
1195
+
1196
+ Prawn::Table::Cell.expects(:draw_cells).times(2).checking do |cells|
1197
+ cells.each do |cell, pt|
1198
+ if cell.content == "header"
1199
+ # Assert that header text is drawn at the same location on each page
1200
+ if @header_location
1201
+ pt.should == @header_location
1202
+ else
1203
+ @header_location = pt
1204
+ end
1205
+ end
1206
+
1207
+ if cell.content == "header2"
1208
+ # Assert that header text is drawn at the same location on each page
1209
+ if @header2_location
1210
+ pt.should == @header2_location
1211
+ else
1212
+ @header2_location = pt
1213
+ end
1214
+ end
1215
+ end
1216
+ end
1217
+ @pdf = Prawn::Document.new
1218
+ @pdf.table(data, :header => 2)
1219
+ end
1220
+
1221
+ it "should_not draw header twice when starting new page" do
1222
+ @pdf = Prawn::Document.new
1223
+ @pdf.y = 0
1224
+ @pdf.table([["Header"], ["Header2"], ["Body"]], :header => 2)
1225
+ output = PDF::Inspector::Text.analyze(@pdf.render)
1226
+ output.strings.should == ["Header", "Header2", "Body"]
1227
+ end
1228
+ end
1229
+ end
1230
+
1231
+ describe "nested tables" do
1232
+ before(:each) do
1233
+ @pdf = Prawn::Document.new
1234
+ @subtable = Prawn::Table.new([["foo"]], @pdf)
1235
+ @table = @pdf.table([[@subtable, "bar"]])
1236
+ end
1237
+
1238
+ it "can be created from an Array" do
1239
+ cell = Prawn::Table::Cell.make(@pdf, [["foo"]])
1240
+ cell.should be_a_kind_of(Prawn::Table::Cell::Subtable)
1241
+ cell.subtable.should be_a_kind_of(Prawn::Table)
1242
+ end
1243
+
1244
+ it "defaults its padding to zero" do
1245
+ @table.cells[0, 0].padding.should == [0, 0, 0, 0]
1246
+ end
1247
+
1248
+ it "has a subtable accessor" do
1249
+ @table.cells[0, 0].subtable.should == @subtable
1250
+ end
1251
+
1252
+ it "determines its dimensions from the subtable" do
1253
+ @table.cells[0, 0].width.should == @subtable.width
1254
+ @table.cells[0, 0].height.should == @subtable.height
1255
+ end
1256
+
1257
+ end
1258
+
1259
+ describe "An invalid table" do
1260
+
1261
+ before(:each) do
1262
+ @pdf = Prawn::Document.new
1263
+ @bad_data = ["Single Nested Array"]
1264
+ end
1265
+
1266
+ it "should raise_error error when invalid table data is given" do
1267
+ lambda {
1268
+ @pdf.table(@bad_data)
1269
+ }.should raise_error(Prawn::Errors::InvalidTableData)
1270
+ end
1271
+
1272
+ it "should raise_error an EmptyTableError with empty table data" do
1273
+ lambda {
1274
+ data = []
1275
+ @pdf = Prawn::Document.new
1276
+ @pdf.table(data)
1277
+ }.should raise_error( Prawn::Errors::EmptyTable )
1278
+ end
1279
+
1280
+ it "should raise_error an EmptyTableError with nil table data" do
1281
+ lambda {
1282
+ data = nil
1283
+ @pdf = Prawn::Document.new
1284
+ @pdf.table(data)
1285
+ }.should raise_error( Prawn::Errors::EmptyTable )
1286
+ end
1287
+
1288
+ end
1289
+
1290
+ end
1291
+
1292
+ describe "colspan / rowspan" do
1293
+ before(:each) { create_pdf }
1294
+
1295
+ it "doesn't raise an error" do
1296
+ lambda {
1297
+ @pdf.table([[{:content => "foo", :colspan => 2, :rowspan => 2}]])
1298
+ }.should_not raise_error
1299
+ end
1300
+
1301
+ it "colspan is properly counted" do
1302
+ t = @pdf.make_table([[{:content => "foo", :colspan => 2}]])
1303
+ t.column_length.should == 2
1304
+ end
1305
+
1306
+ it "rowspan is properly counted" do
1307
+ t = @pdf.make_table([[{:content => "foo", :rowspan => 2}]])
1308
+ t.row_length.should == 2
1309
+ end
1310
+
1311
+ it "raises if colspan or rowspan are called after layout" do
1312
+ lambda {
1313
+ @pdf.table([["foo"]]) { cells[0, 0].colspan = 2 }
1314
+ }.should raise_error(Prawn::Errors::InvalidTableSpan)
1315
+
1316
+ lambda {
1317
+ @pdf.table([["foo"]]) { cells[0, 0].rowspan = 2 }
1318
+ }.should raise_error(Prawn::Errors::InvalidTableSpan)
1319
+ end
1320
+
1321
+ it "raises when spans overlap" do
1322
+ lambda {
1323
+ @pdf.table([["foo", {:content => "bar", :rowspan => 2}],
1324
+ [{:content => "baz", :colspan => 2}]])
1325
+ }.should raise_error(Prawn::Errors::InvalidTableSpan)
1326
+ end
1327
+
1328
+ it "table and cell width account for colspan" do
1329
+ t = @pdf.table([["a", {:content => "b", :colspan => 2}]],
1330
+ :column_widths => [100, 100, 100])
1331
+ spanned = t.cells[0, 1]
1332
+ spanned.colspan.should == 2
1333
+ t.width.should == 300
1334
+ t.cells.min_width.should == 300
1335
+ t.cells.max_width.should == 300
1336
+ spanned.width.should == 200
1337
+ end
1338
+
1339
+ it "table and cell height account for rowspan" do
1340
+ t = @pdf.table([["a"], [{:content => "b", :rowspan => 2}]]) do
1341
+ row(0..2).height = 100
1342
+ end
1343
+ spanned = t.cells[1, 0]
1344
+ spanned.rowspan.should == 2
1345
+ t.height.should == 300
1346
+ spanned.height.should == 200
1347
+ end
1348
+
1349
+ it "provides the full content_width as drawing space" do
1350
+ w = @pdf.make_table([["foo"]]).cells[0, 0].content_width
1351
+
1352
+ t = @pdf.make_table([[{:content => "foo", :colspan => 2}]])
1353
+ t.cells[0, 0].spanned_content_width.should == w
1354
+ end
1355
+
1356
+ it "dummy cells are not drawn" do
1357
+ # make a fake master cell for the dummy cell to slave to
1358
+ t = @pdf.make_table([[{:content => "foo", :colspan => 2}]])
1359
+
1360
+ # drawing just a dummy cell should_not ink
1361
+ @pdf.expects(:stroke_line).never
1362
+ @pdf.expects(:draw_text!).never
1363
+ Prawn::Table::Cell.draw_cells([t.cells[0, 1]])
1364
+ end
1365
+
1366
+ it "dummy cells do not add any height or width" do
1367
+ t1 = @pdf.table([["foo"]])
1368
+
1369
+ t2 = @pdf.table([[{:content => "foo", :colspan => 2}]])
1370
+ t2.width.should == t1.width
1371
+
1372
+ t3 = @pdf.table([[{:content => "foo", :rowspan => 2}]])
1373
+ t3.height.should == t1.height
1374
+ end
1375
+
1376
+ it "dummy cells ignored by #style" do
1377
+ t = @pdf.table([[{:content => "blah", :colspan => 2}]],
1378
+ :cell_style => { :size => 9 })
1379
+ t.cells[0, 0].size.should == 9
1380
+ end
1381
+
1382
+ context "inheriting master cell styles from dummy cell" do
1383
+ # Relatively full coverage for all these attributes that should be
1384
+ # inherited.
1385
+ [["border_X_width", 20],
1386
+ ["border_X_color", "123456"],
1387
+ ["padding_X", 20]].each do |attribute, val|
1388
+ attribute_right = attribute.sub("X", "right")
1389
+ attribute_left = attribute.sub("X", "left")
1390
+ attribute_bottom = attribute.sub("X", "bottom")
1391
+ attribute_top = attribute.sub("X", "top")
1392
+
1393
+ specify "#{attribute_right} of right column is inherited" do
1394
+ t = @pdf.table([[{:content => "blah", :colspan => 2}]]) do |table|
1395
+ table.column(1).send("#{attribute_right}=", val)
1396
+ end
1397
+
1398
+ t.cells[0, 0].send(attribute_right).should == val
1399
+ end
1400
+
1401
+ specify "#{attribute_bottom} of bottom row is inherited" do
1402
+ t = @pdf.table([[{:content => "blah", :rowspan => 2}]]) do |table|
1403
+ table.row(1).send("#{attribute_bottom}=", val)
1404
+ end
1405
+
1406
+ t.cells[0, 0].send(attribute_bottom).should == val
1407
+ end
1408
+
1409
+ specify "#{attribute_left} of right column is not inherited" do
1410
+ t = @pdf.table([[{:content => "blah", :colspan => 2}]]) do |table|
1411
+ table.column(1).send("#{attribute_left}=", val)
1412
+ end
1413
+
1414
+ t.cells[0, 0].send(attribute_left).should_not == val
1415
+ end
1416
+
1417
+ specify "#{attribute_right} of interior column is not inherited" do
1418
+ t = @pdf.table([[{:content => "blah", :colspan => 3}]]) do |table|
1419
+ table.column(1).send("#{attribute_right}=", val)
1420
+ end
1421
+
1422
+ t.cells[0, 0].send(attribute_right).should_not == val
1423
+ end
1424
+
1425
+ specify "#{attribute_bottom} of interior row is not inherited" do
1426
+ t = @pdf.table([[{:content => "blah", :rowspan => 3}]]) do |table|
1427
+ table.row(1).send("#{attribute_bottom}=", val)
1428
+ end
1429
+
1430
+ t.cells[0, 0].send(attribute_bottom).should_not == val
1431
+ end
1432
+
1433
+ specify "#{attribute_top} of bottom row is not inherited" do
1434
+ t = @pdf.table([[{:content => "blah", :rowspan => 2}]]) do |table|
1435
+ table.row(1).send("#{attribute_top}=", val)
1436
+ end
1437
+
1438
+ t.cells[0, 0].send(attribute_top).should_not == val
1439
+ end
1440
+ end
1441
+ end
1442
+
1443
+ it "splits natural width between cols in the group" do
1444
+ t = @pdf.table([[{:content => "foo", :colspan => 2}]])
1445
+ widths = t.column_widths
1446
+ widths[0].should == widths[1]
1447
+ end
1448
+
1449
+ it "splits natural width between cols when width is increased" do
1450
+ t = @pdf.table([[{:content => "foo", :colspan => 2}]],
1451
+ :width => @pdf.bounds.width)
1452
+ widths = t.column_widths
1453
+ widths[0].should == widths[1]
1454
+ end
1455
+
1456
+ it "splits min-width between cols in the group" do
1457
+ # Since column_widths, when reducing column widths, reduces proportional to
1458
+ # the remaining width after each column's min width, we must ensure that the
1459
+ # min-width is split proportionally in order to ensure the width is still
1460
+ # split evenly when the width is reduced. (See "splits natural width between
1461
+ # cols when width is reduced".)
1462
+ t = @pdf.table([[{:content => "foo", :colspan => 2}]],
1463
+ :width => 20)
1464
+ t.column(0).min_width.should == t.column(1).min_width
1465
+ end
1466
+
1467
+ it "splits natural width between cols when width is reduced" do
1468
+ t = @pdf.table([[{:content => "foo", :colspan => 2}]],
1469
+ :width => 20)
1470
+ widths = t.column_widths
1471
+ widths[0].should == widths[1]
1472
+ end
1473
+
1474
+ it "honors a large, explicitly set table width" do
1475
+ t = @pdf.table([[{:content => "AAAAAAAAAA", :colspan => 3}],
1476
+ ["A", "B", "C"]],
1477
+ :width => 400)
1478
+
1479
+ t.column_widths.inject(0) { |sum, w| sum + w }.
1480
+ should be_within(0.01).of(400)
1481
+ end
1482
+
1483
+ it "honors a small, explicitly set table width" do
1484
+ t = @pdf.table([[{:content => "Lorem ipsum dolor sit amet " * 20,
1485
+ :colspan => 3}],
1486
+ ["A", "B", "C"]],
1487
+ :width => 200)
1488
+ t.column_widths.inject(0) { |sum, w| sum + w }.
1489
+ should be_within(0.01).of(200)
1490
+ end
1491
+
1492
+ it "splits natural_content_height between rows in the group" do
1493
+ t = @pdf.table([[{:content => "foo", :rowspan => 2}]])
1494
+ heights = t.row_heights
1495
+ heights[0].should == heights[1]
1496
+ end
1497
+
1498
+ it "skips column numbers that have been col-spanned" do
1499
+ t = @pdf.table([["a", "b", {:content => "c", :colspan => 3}, "d"]])
1500
+ t.cells[0, 0].content.should == "a"
1501
+ t.cells[0, 1].content.should == "b"
1502
+ t.cells[0, 2].content.should == "c"
1503
+ t.cells[0, 3].should be_a_kind_of(Prawn::Table::Cell::SpanDummy)
1504
+ t.cells[0, 4].should be_a_kind_of(Prawn::Table::Cell::SpanDummy)
1505
+ t.cells[0, 5].content.should == "d"
1506
+ end
1507
+
1508
+ it "skips row/col positions that have been row-spanned" do
1509
+ t = @pdf.table([["a", {:content => "b", :colspan => 2, :rowspan => 2}, "c"],
1510
+ ["d", "e"],
1511
+ ["f", "g", "h", "i"]])
1512
+ t.cells[0, 0].content.should == "a"
1513
+ t.cells[0, 1].content.should == "b"
1514
+ t.cells[0, 2].should be_a_kind_of(Prawn::Table::Cell::SpanDummy)
1515
+ t.cells[0, 3].content.should == "c"
1516
+
1517
+ t.cells[1, 0].content.should == "d"
1518
+ t.cells[1, 1].should be_a_kind_of(Prawn::Table::Cell::SpanDummy)
1519
+ t.cells[1, 2].should be_a_kind_of(Prawn::Table::Cell::SpanDummy)
1520
+ t.cells[1, 3].content.should == "e"
1521
+
1522
+ t.cells[2, 0].content.should == "f"
1523
+ t.cells[2, 1].content.should == "g"
1524
+ t.cells[2, 2].content.should == "h"
1525
+ t.cells[2, 3].content.should == "i"
1526
+ end
1527
+ end