pdf 0.1.0 → 0.1.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/execution/00-overview.md +121 -0
  3. data/.claude/execution/01-core.md +324 -0
  4. data/.claude/execution/02-renderer.md +237 -0
  5. data/.claude/execution/03-components.md +551 -0
  6. data/.claude/execution/04-builders.md +322 -0
  7. data/.claude/execution/05-view-layout.md +362 -0
  8. data/.claude/execution/06-evaluators.md +494 -0
  9. data/.claude/execution/07-entry-extensibility.md +435 -0
  10. data/.claude/execution/08-integration-tests.md +978 -0
  11. data/Rakefile +7 -3
  12. data/examples/01_basic_invoice.rb +139 -0
  13. data/examples/02_report_with_layout.rb +266 -0
  14. data/examples/03_inherited_views.rb +318 -0
  15. data/examples/04_conditional_content.rb +421 -0
  16. data/examples/05_custom_components.rb +442 -0
  17. data/examples/README.md +123 -0
  18. data/lib/pdf/blueprint.rb +50 -0
  19. data/lib/pdf/builders/content_builder.rb +96 -0
  20. data/lib/pdf/builders/footer_builder.rb +24 -0
  21. data/lib/pdf/builders/header_builder.rb +31 -0
  22. data/lib/pdf/component.rb +43 -0
  23. data/lib/pdf/components/alert.rb +42 -0
  24. data/lib/pdf/components/context.rb +16 -0
  25. data/lib/pdf/components/date.rb +28 -0
  26. data/lib/pdf/components/heading.rb +12 -0
  27. data/lib/pdf/components/hr.rb +12 -0
  28. data/lib/pdf/components/logo.rb +15 -0
  29. data/lib/pdf/components/paragraph.rb +12 -0
  30. data/lib/pdf/components/qr_code.rb +38 -0
  31. data/lib/pdf/components/spacer.rb +11 -0
  32. data/lib/pdf/components/span.rb +12 -0
  33. data/lib/pdf/components/subtitle.rb +12 -0
  34. data/lib/pdf/components/table.rb +48 -0
  35. data/lib/pdf/components/title.rb +12 -0
  36. data/lib/pdf/content_evaluator.rb +218 -0
  37. data/lib/pdf/dynamic_components.rb +17 -0
  38. data/lib/pdf/footer_evaluator.rb +66 -0
  39. data/lib/pdf/header_evaluator.rb +56 -0
  40. data/lib/pdf/layout.rb +61 -0
  41. data/lib/pdf/renderer.rb +153 -0
  42. data/lib/pdf/resolver.rb +36 -0
  43. data/lib/pdf/version.rb +1 -1
  44. data/lib/pdf/view.rb +113 -0
  45. data/lib/pdf.rb +74 -1
  46. metadata +127 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19e2ef9c0e0482e085bcdefd46dda508db90747f329907bceac9013519389b61
4
- data.tar.gz: 763e676924914511fc86272f238b8b65700820005b968806ebccc080c6b492c2
3
+ metadata.gz: a65082843a9b93d0e95848671c5e58ba2921e838c168a76dd9cca4b14cd76044
4
+ data.tar.gz: a6dcf50015479680b09fc8a36039467fa188442781622e086ea12a0a43bbfb5d
5
5
  SHA512:
6
- metadata.gz: 97298fcaa03b79cb466602cdb9fcfb48a0be7d42606a885219d36cb98a426df6e1461e5c504f44472f03504202e23f66f93588bb7cee55653f2114d92e81aae2
7
- data.tar.gz: 405ff975805cb134173d87ed54492e34b371d1ba60d2b70e0140e713ce95c2cc2570f64275779d2ae432e1c4ce4db3f6bbff8c4d0616eb3013bf15856f3eadf0
6
+ metadata.gz: 306faa44b40d9be836752330a24198c7897892fc0b149e801fc08f72bfb91775f754c62e7a9e7b22cb28ea170389ffe6842e5153713b6d05181e10d915e3e344
7
+ data.tar.gz: dda4d0483b77b8a3ba2371e1bca8d4556d33ecaeb3c71a444b88869eb28ebfaab856ae99f9a993425b4edc6976fac425e978a5a4f885578b1d390b6ed2a8558d
@@ -0,0 +1,121 @@
1
+ # PDF Gem - Execution Overview
2
+
3
+ ## Architecture
4
+
5
+ ```
6
+ ┌─────────────────────────────────────────────────────────┐
7
+ │ User Code │
8
+ │ class MyReport < Pdf::View │
9
+ │ title "Report" │
10
+ │ table :items │
11
+ │ end │
12
+ └─────────────────────────────────────────────────────────┘
13
+
14
+
15
+ ┌─────────────────────────────────────────────────────────┐
16
+ │ Builders (DSL) │
17
+ │ ContentBuilder, HeaderBuilder, FooterBuilder │
18
+ │ Translate DSL calls → Blueprint │
19
+ │ (uses DynamicComponents mixin for extensibility) │
20
+ └─────────────────────────────────────────────────────────┘
21
+
22
+
23
+ ┌─────────────────────────────────────────────────────────┐
24
+ │ Blueprint │
25
+ │ Stores element declarations as data │
26
+ │ [{type: :title, args: ["Report"], options: {}}] │
27
+ └─────────────────────────────────────────────────────────┘
28
+
29
+
30
+ ┌─────────────────────────────────────────────────────────┐
31
+ │ Evaluators │
32
+ │ ContentEvaluator, HeaderEvaluator, FooterEvaluator │
33
+ │ Resolve symbols, dispatch to components │
34
+ │ Handle conditionals (render_if), page numbers │
35
+ └─────────────────────────────────────────────────────────┘
36
+
37
+
38
+ ┌─────────────────────────────────────────────────────────┐
39
+ │ Components │
40
+ │ Title, Paragraph, Table, Alert, Hr, Date, QrCode... │
41
+ │ Each component renders using primitives │
42
+ │ NEVER swallow errors - always raise │
43
+ └─────────────────────────────────────────────────────────┘
44
+
45
+
46
+ ┌─────────────────────────────────────────────────────────┐
47
+ │ Renderer (Primitives) │
48
+ │ text, box, line, image, table_data, raw │
49
+ │ Thin wrapper over Prawn │
50
+ └─────────────────────────────────────────────────────────┘
51
+
52
+
53
+ ┌─────────────────────────────────────────────────────────┐
54
+ │ Prawn │
55
+ │ PDF generation │
56
+ └─────────────────────────────────────────────────────────┘
57
+ ```
58
+
59
+ ## Design Principles
60
+
61
+ 1. **No silent failures** - All errors bubble up. Client code handles exceptions.
62
+ 2. **Proper inheritance** - Child views inherit parent blueprint via `dup`.
63
+ 3. **DRY** - Shared behavior extracted to modules (e.g., `DynamicComponents`).
64
+ 4. **Escape hatches** - `raw` block for direct Prawn access when needed.
65
+
66
+ ## Execution Phases
67
+
68
+ | Phase | Focus | Files |
69
+ |-------|-------|-------|
70
+ | 01 | Core Infrastructure | blueprint.rb, resolver.rb, component.rb, dynamic_components.rb |
71
+ | 02 | Renderer Primitives | renderer.rb |
72
+ | 03 | Built-in Components | components/*.rb |
73
+ | 04 | Builders (DSL) | builders/*.rb |
74
+ | 05 | View & Layout | view.rb, layout.rb |
75
+ | 06 | Evaluators | *_evaluator.rb |
76
+ | 07 | Entry Point & Extensibility | pdf.rb, register_component |
77
+
78
+ ## Dependencies
79
+
80
+ ```ruby
81
+ spec.add_dependency "prawn", "~> 2.4"
82
+ spec.add_dependency "prawn-table", "~> 0.2"
83
+ spec.add_dependency "rqrcode", "~> 2.0" # QR code generation
84
+ ```
85
+
86
+ ## File Structure
87
+
88
+ ```
89
+ lib/
90
+ pdf.rb
91
+ pdf/
92
+ version.rb
93
+ blueprint.rb
94
+ resolver.rb
95
+ component.rb
96
+ dynamic_components.rb
97
+ renderer.rb
98
+ view.rb
99
+ layout.rb
100
+ content_evaluator.rb
101
+ header_evaluator.rb
102
+ footer_evaluator.rb
103
+ builders/
104
+ content_builder.rb
105
+ header_builder.rb
106
+ footer_builder.rb
107
+ components/
108
+ title.rb
109
+ subtitle.rb
110
+ heading.rb
111
+ paragraph.rb
112
+ span.rb
113
+ date.rb
114
+ hr.rb
115
+ spacer.rb
116
+ alert.rb
117
+ table.rb
118
+ logo.rb
119
+ qr_code.rb
120
+ context.rb
121
+ ```
@@ -0,0 +1,324 @@
1
+ # Phase 1: Core Infrastructure
2
+
3
+ ## Files
4
+ - `lib/pdf/blueprint.rb`
5
+ - `lib/pdf/resolver.rb`
6
+ - `lib/pdf/component.rb`
7
+ - `lib/pdf/dynamic_components.rb`
8
+
9
+ ---
10
+
11
+ ## 1.1 Blueprint
12
+
13
+ Storage for DSL declarations. Elements stored as data, evaluated later.
14
+
15
+ ```ruby
16
+ # lib/pdf/blueprint.rb
17
+ # frozen_string_literal: true
18
+
19
+ module Pdf
20
+ class Blueprint
21
+ attr_reader :elements, :layout_class, :metadata
22
+
23
+ def initialize
24
+ @elements = []
25
+ @layout_class = nil
26
+ @metadata = {}
27
+ end
28
+
29
+ def add(type, *args, **options, &block)
30
+ element = { type: type, args: args, options: options }
31
+ element[:block] = block if block
32
+ @elements << element
33
+ end
34
+
35
+ def set_layout(klass)
36
+ @layout_class = klass
37
+ end
38
+
39
+ def set_metadata(key, value)
40
+ @metadata[key] = value
41
+ end
42
+
43
+ def empty?
44
+ @elements.empty? && @layout_class.nil? && @metadata.empty?
45
+ end
46
+
47
+ def dup
48
+ copy = Blueprint.new
49
+ copy.instance_variable_set(:@elements, deep_dup_elements)
50
+ copy.instance_variable_set(:@layout_class, @layout_class)
51
+ copy.instance_variable_set(:@metadata, @metadata.dup)
52
+ copy
53
+ end
54
+
55
+ private
56
+
57
+ def deep_dup_elements
58
+ @elements.map do |el|
59
+ duped = el.dup
60
+ duped[:args] = el[:args].dup
61
+ duped[:options] = el[:options].dup
62
+ duped
63
+ end
64
+ end
65
+ end
66
+ end
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 1.2 Resolver
72
+
73
+ Resolves symbols to values at render time.
74
+
75
+ Resolution order:
76
+ 1. Method on context → call it
77
+ 2. Key in `data` hash → return value
78
+ 3. Raise `ResolutionError`
79
+
80
+ ```ruby
81
+ # lib/pdf/resolver.rb
82
+ # frozen_string_literal: true
83
+
84
+ module Pdf
85
+ class ResolutionError < StandardError; end
86
+
87
+ class Resolver
88
+ def initialize(context)
89
+ @context = context
90
+ end
91
+
92
+ def resolve(value)
93
+ case value
94
+ when Symbol then resolve_symbol(value)
95
+ when Proc then @context.instance_exec(&value)
96
+ when Array then value.map { |v| resolve(v) }
97
+ when Hash then value.transform_values { |v| resolve(v) }
98
+ else value
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def resolve_symbol(sym)
105
+ if @context.respond_to?(sym, true)
106
+ return @context.send(sym)
107
+ end
108
+
109
+ if @context.respond_to?(:data) && @context.data.is_a?(Hash)
110
+ return @context.data[sym] if @context.data.key?(sym)
111
+ return @context.data[sym.to_s] if @context.data.key?(sym.to_s)
112
+ end
113
+
114
+ raise ResolutionError, "Cannot resolve :#{sym}"
115
+ end
116
+ end
117
+ end
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 1.3 Component Base
123
+
124
+ Base class for all components. Components receive Prawn document and render using primitives.
125
+
126
+ ```ruby
127
+ # lib/pdf/component.rb
128
+ # frozen_string_literal: true
129
+
130
+ module Pdf
131
+ class Component
132
+ attr_reader :pdf
133
+
134
+ def initialize(pdf)
135
+ @pdf = pdf
136
+ end
137
+
138
+ def render(...)
139
+ raise NotImplementedError, "#{self.class}#render not implemented"
140
+ end
141
+
142
+ private
143
+
144
+ # Primitive helpers (delegate to pdf)
145
+
146
+ def text(content, **options)
147
+ @pdf.text content.to_s, **options
148
+ end
149
+
150
+ def move_down(amount)
151
+ @pdf.move_down amount
152
+ end
153
+
154
+ def stroke_horizontal_rule
155
+ @pdf.stroke_horizontal_rule
156
+ end
157
+
158
+ def bounding_box(point, **options, &block)
159
+ @pdf.bounding_box(point, **options, &block)
160
+ end
161
+
162
+ def cursor
163
+ @pdf.cursor
164
+ end
165
+
166
+ def bounds
167
+ @pdf.bounds
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ ---
174
+
175
+ ## 1.4 DynamicComponents Module
176
+
177
+ Shared mixin for dynamic component support. DRYs up method_missing across builders and views.
178
+
179
+ ```ruby
180
+ # lib/pdf/dynamic_components.rb
181
+ # frozen_string_literal: true
182
+
183
+ module Pdf
184
+ module DynamicComponents
185
+ def method_missing(name, *args, **options, &block)
186
+ if Pdf.component_registered?(name)
187
+ @blueprint.add(name, *args, **options, &block)
188
+ else
189
+ super
190
+ end
191
+ end
192
+
193
+ def respond_to_missing?(name, include_private = false)
194
+ Pdf.component_registered?(name) || super
195
+ end
196
+ end
197
+ end
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Tests
203
+
204
+ ```ruby
205
+ # test/pdf/blueprint_test.rb
206
+ # frozen_string_literal: true
207
+
208
+ require "test_helper"
209
+
210
+ class BlueprintTest < Minitest::Test
211
+ def setup
212
+ @blueprint = Pdf::Blueprint.new
213
+ end
214
+
215
+ def test_add_stores_element
216
+ @blueprint.add(:title, "Hello", size: 20)
217
+ element = @blueprint.elements.first
218
+ assert_equal :title, element[:type]
219
+ assert_equal ["Hello"], element[:args]
220
+ assert_equal({ size: 20 }, element[:options])
221
+ end
222
+
223
+ def test_add_stores_block
224
+ @blueprint.add(:section, "Items") { table :items }
225
+ assert_kind_of Proc, @blueprint.elements.first[:block]
226
+ end
227
+
228
+ def test_dup_creates_independent_copy
229
+ @blueprint.add(:title, "Original")
230
+ copy = @blueprint.dup
231
+ copy.add(:title, "Copy")
232
+ assert_equal 1, @blueprint.elements.size
233
+ end
234
+
235
+ def test_empty_returns_true_for_new_blueprint
236
+ assert @blueprint.empty?
237
+ end
238
+
239
+ def test_empty_returns_false_after_adding_element
240
+ @blueprint.add(:title, "Hello")
241
+ refute @blueprint.empty?
242
+ end
243
+ end
244
+
245
+ # test/pdf/resolver_test.rb
246
+ # frozen_string_literal: true
247
+
248
+ require "test_helper"
249
+
250
+ class ResolverTest < Minitest::Test
251
+ def setup
252
+ context_class = Class.new do
253
+ attr_reader :data
254
+ def initialize(data) = @data = data
255
+ def computed = "computed:#{data[:raw]}"
256
+ end
257
+ @context = context_class.new({ raw: "value", key: "data_value" })
258
+ @resolver = Pdf::Resolver.new(@context)
259
+ end
260
+
261
+ def test_returns_strings_as_is
262
+ assert_equal "hello", @resolver.resolve("hello")
263
+ end
264
+
265
+ def test_resolves_method
266
+ assert_equal "computed:value", @resolver.resolve(:computed)
267
+ end
268
+
269
+ def test_resolves_data_key
270
+ assert_equal "data_value", @resolver.resolve(:key)
271
+ end
272
+
273
+ def test_raises_on_unknown
274
+ assert_raises(Pdf::ResolutionError) { @resolver.resolve(:unknown) }
275
+ end
276
+ end
277
+
278
+ # test/pdf/component_test.rb
279
+ # frozen_string_literal: true
280
+
281
+ require "test_helper"
282
+
283
+ class ComponentTest < Minitest::Test
284
+ def test_requires_render_implementation
285
+ pdf = Object.new
286
+ component = Pdf::Component.new(pdf)
287
+ assert_raises(NotImplementedError) { component.render }
288
+ end
289
+ end
290
+
291
+ # test/pdf/dynamic_components_test.rb
292
+ # frozen_string_literal: true
293
+
294
+ require "test_helper"
295
+
296
+ class DynamicComponentsTest < Minitest::Test
297
+ def setup
298
+ @builder_class = Class.new do
299
+ include Pdf::DynamicComponents
300
+ attr_reader :blueprint
301
+
302
+ def initialize
303
+ @blueprint = Pdf::Blueprint.new
304
+ end
305
+ end
306
+ Pdf.register_component(:badge, Class.new(Pdf::Component))
307
+ end
308
+
309
+ def teardown
310
+ Pdf.instance_variable_get(:@components).delete(:badge)
311
+ end
312
+
313
+ def test_handles_registered_components
314
+ builder = @builder_class.new
315
+ builder.badge("NEW", color: "red")
316
+ assert_equal :badge, builder.blueprint.elements.first[:type]
317
+ end
318
+
319
+ def test_raises_for_unknown_methods
320
+ builder = @builder_class.new
321
+ assert_raises(NoMethodError) { builder.unknown_thing }
322
+ end
323
+ end
324
+ ```
@@ -0,0 +1,237 @@
1
+ # Phase 2: Renderer (Primitives)
2
+
3
+ ## Files
4
+ - `lib/pdf/renderer.rb`
5
+
6
+ ---
7
+
8
+ ## Renderer
9
+
10
+ Thin wrapper over Prawn. Provides primitives that components use.
11
+
12
+ **Key principles:**
13
+ - Never swallow errors - let them bubble up
14
+ - Consistent default margins (shared constant)
15
+ - `raw` escape hatch for direct Prawn access
16
+
17
+ ```ruby
18
+ # lib/pdf/renderer.rb
19
+ # frozen_string_literal: true
20
+
21
+ require "prawn"
22
+ require "prawn/table"
23
+
24
+ module Pdf
25
+ # Default margins used by both Renderer and Layout
26
+ DEFAULT_MARGINS = { top: 72, right: 36, bottom: 72, left: 36 }.freeze
27
+
28
+ class Renderer
29
+ attr_reader :pdf
30
+
31
+ def initialize
32
+ @pdf = nil
33
+ @footer_callback = nil
34
+ end
35
+
36
+ def setup(margins: {})
37
+ m = Pdf::DEFAULT_MARGINS.merge(margins)
38
+ @pdf = Prawn::Document.new(
39
+ margin: [m[:top], m[:right], m[:bottom], m[:left]]
40
+ )
41
+ setup_fonts
42
+ self
43
+ end
44
+
45
+ def finalize
46
+ apply_footer if @footer_callback
47
+ @pdf.render
48
+ end
49
+
50
+ # --- Primitives ---
51
+
52
+ def text(content, **options)
53
+ @pdf.text content.to_s, **options
54
+ end
55
+
56
+ def text_box(content, at:, **options)
57
+ @pdf.text_box content.to_s, at: at, **options
58
+ end
59
+
60
+ def move_down(amount)
61
+ @pdf.move_down amount
62
+ end
63
+
64
+ def stroke_horizontal_rule
65
+ @pdf.stroke_horizontal_rule
66
+ end
67
+
68
+ def start_new_page
69
+ @pdf.start_new_page
70
+ end
71
+
72
+ def cursor
73
+ @pdf.cursor
74
+ end
75
+
76
+ def bounds
77
+ @pdf.bounds
78
+ end
79
+
80
+ def bounding_box(point, **options, &block)
81
+ @pdf.bounding_box(point, **options, &block)
82
+ end
83
+
84
+ def canvas(&block)
85
+ @pdf.canvas(&block)
86
+ end
87
+
88
+ def float(&block)
89
+ @pdf.float(&block)
90
+ end
91
+
92
+ # Image embedding - raises if file not found (no silent failures)
93
+ def image(path, **options)
94
+ raise Pdf::FileNotFoundError, "Image not found: #{path}" unless File.exist?(path)
95
+
96
+ @pdf.image path, **options
97
+ end
98
+
99
+ # Image from string data (for generated images like QR codes)
100
+ def image_data(data, **options)
101
+ @pdf.image StringIO.new(data), **options
102
+ end
103
+
104
+ def fill_color(color)
105
+ @pdf.fill_color color
106
+ end
107
+
108
+ def stroke_color(color)
109
+ @pdf.stroke_color color
110
+ end
111
+
112
+ def line_width(width)
113
+ @pdf.line_width width
114
+ end
115
+
116
+ def fill_and_stroke_rounded_rectangle(point, width, height, radius)
117
+ @pdf.fill_and_stroke_rounded_rectangle point, width, height, radius
118
+ end
119
+
120
+ def draw_text(content, at:, **options)
121
+ @pdf.draw_text content.to_s, at: at, **options
122
+ end
123
+
124
+ def width_of(content, **options)
125
+ @pdf.width_of content.to_s, **options
126
+ end
127
+
128
+ def height_of(content, **options)
129
+ @pdf.height_of content.to_s, **options
130
+ end
131
+
132
+ def table(data, **options, &block)
133
+ @pdf.table(data, **options, &block)
134
+ end
135
+
136
+ def page_number
137
+ @pdf.page_number
138
+ end
139
+
140
+ def page_count
141
+ @pdf.page_count
142
+ end
143
+
144
+ def repeat(page_filter, options = {}, &block)
145
+ @pdf.repeat(page_filter, options, &block)
146
+ end
147
+
148
+ # --- Escape hatch for direct Prawn access ---
149
+
150
+ def raw(&block)
151
+ yield @pdf
152
+ end
153
+
154
+ # --- Footer setup (deferred) ---
155
+
156
+ def setup_footer(&block)
157
+ @footer_callback = block
158
+ end
159
+
160
+ private
161
+
162
+ def setup_fonts
163
+ # Use Helvetica by default (built into Prawn)
164
+ # Users can override in their app
165
+ end
166
+
167
+ def apply_footer
168
+ @footer_callback&.call(@pdf)
169
+ end
170
+ end
171
+ end
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Tests
177
+
178
+ ```ruby
179
+ # test/pdf/renderer_test.rb
180
+ # frozen_string_literal: true
181
+
182
+ require "test_helper"
183
+
184
+ class RendererTest < Minitest::Test
185
+ def setup
186
+ @renderer = Pdf::Renderer.new.setup
187
+ end
188
+
189
+ def test_setup_creates_prawn_document
190
+ assert_kind_of Prawn::Document, @renderer.pdf
191
+ end
192
+
193
+ def test_setup_accepts_custom_margins
194
+ r = Pdf::Renderer.new.setup(margins: { top: 100 })
195
+ assert_kind_of Prawn::Document, r.pdf
196
+ end
197
+
198
+ def test_uses_default_margins
199
+ expected = { top: 72, right: 36, bottom: 72, left: 36 }
200
+ assert_equal expected, Pdf::DEFAULT_MARGINS
201
+ end
202
+
203
+ def test_finalize_returns_pdf_binary
204
+ result = @renderer.finalize
205
+ assert result.start_with?("%PDF")
206
+ end
207
+
208
+ def test_text_delegates_to_prawn
209
+ # Just verify it doesn't raise
210
+ @renderer.text("Hello", size: 12)
211
+ end
212
+
213
+ def test_image_raises_file_not_found_error
214
+ error = assert_raises(Pdf::FileNotFoundError) do
215
+ @renderer.image("/nonexistent/path.png")
216
+ end
217
+ assert_match(/Image not found/, error.message)
218
+ end
219
+
220
+ def test_raw_yields_prawn_document
221
+ yielded = nil
222
+ @renderer.raw { |pdf| yielded = pdf }
223
+ assert_equal @renderer.pdf, yielded
224
+ end
225
+
226
+ def test_raw_allows_direct_prawn_operations
227
+ @renderer.raw { |pdf| pdf.stroke_circle [100, 100], 50 }
228
+ end
229
+
230
+ def test_setup_footer_stores_callback_for_finalize
231
+ called = false
232
+ @renderer.setup_footer { |_pdf| called = true }
233
+ @renderer.finalize
234
+ assert called
235
+ end
236
+ end
237
+ ```