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
@@ -0,0 +1,435 @@
1
+ # Phase 7: Entry Point & Extensibility
2
+
3
+ ## Files
4
+ - `lib/pdf.rb`
5
+
6
+ ---
7
+
8
+ ## Entry Point
9
+
10
+ Main entry point. Requires all files, provides component registry and error classes.
11
+
12
+ **Changes:**
13
+ - Added `FileNotFoundError` for explicit file errors
14
+ - Removed `PageNumber` component (handled by FooterEvaluator)
15
+ - Added `Date` and `Spacer` components
16
+ - Added `DynamicComponents` module
17
+ - Added `rqrcode` require
18
+
19
+ ```ruby
20
+ # lib/pdf.rb
21
+ # frozen_string_literal: true
22
+
23
+ require "prawn"
24
+ require "prawn/table"
25
+ require "rqrcode"
26
+
27
+ require_relative "pdf/version"
28
+ require_relative "pdf/blueprint"
29
+ require_relative "pdf/resolver"
30
+ require_relative "pdf/component"
31
+ require_relative "pdf/dynamic_components"
32
+ require_relative "pdf/renderer"
33
+
34
+ require_relative "pdf/builders/content_builder"
35
+ require_relative "pdf/builders/header_builder"
36
+ require_relative "pdf/builders/footer_builder"
37
+
38
+ require_relative "pdf/components/title"
39
+ require_relative "pdf/components/subtitle"
40
+ require_relative "pdf/components/heading"
41
+ require_relative "pdf/components/paragraph"
42
+ require_relative "pdf/components/span"
43
+ require_relative "pdf/components/date"
44
+ require_relative "pdf/components/hr"
45
+ require_relative "pdf/components/spacer"
46
+ require_relative "pdf/components/alert"
47
+ require_relative "pdf/components/table"
48
+ require_relative "pdf/components/logo"
49
+ require_relative "pdf/components/qr_code"
50
+ require_relative "pdf/components/context"
51
+
52
+ require_relative "pdf/content_evaluator"
53
+ require_relative "pdf/header_evaluator"
54
+ require_relative "pdf/footer_evaluator"
55
+
56
+ require_relative "pdf/view"
57
+ require_relative "pdf/layout"
58
+
59
+ module Pdf
60
+ class Error < StandardError; end
61
+ class FileNotFoundError < Error; end
62
+
63
+ # Component registry (PageNumber is handled by FooterEvaluator, not here)
64
+ @components = {
65
+ title: Components::Title,
66
+ subtitle: Components::Subtitle,
67
+ heading: Components::Heading,
68
+ paragraph: Components::Paragraph,
69
+ span: Components::Span,
70
+ date: Components::Date,
71
+ hr: Components::Hr,
72
+ spacer: Components::Spacer,
73
+ alert: Components::Alert,
74
+ table: Components::Table,
75
+ logo: Components::Logo,
76
+ qr_code: Components::QrCode,
77
+ context: Components::Context
78
+ }
79
+
80
+ class << self
81
+ # Register a custom component
82
+ #
83
+ # @param name [Symbol] Component name for DSL
84
+ # @param klass [Class] Component class (must inherit from Pdf::Component)
85
+ #
86
+ # @example
87
+ # class MyBadge < Pdf::Component
88
+ # def render(text, color: "blue")
89
+ # # ...
90
+ # end
91
+ # end
92
+ #
93
+ # Pdf.register_component(:badge, MyBadge)
94
+ #
95
+ # class MyReport < Pdf::View
96
+ # badge "NEW", color: "red"
97
+ # end
98
+ #
99
+ def register_component(name, klass)
100
+ unless klass < Component
101
+ raise ArgumentError, "#{klass} must inherit from Pdf::Component"
102
+ end
103
+
104
+ @components[name.to_sym] = klass
105
+ end
106
+
107
+ # Check if component is registered
108
+ #
109
+ # @param name [Symbol] Component name
110
+ # @return [Boolean]
111
+ #
112
+ def component_registered?(name)
113
+ @components.key?(name.to_sym)
114
+ end
115
+
116
+ # Get component class
117
+ #
118
+ # @param name [Symbol] Component name
119
+ # @return [Class]
120
+ # @raise [Error] if not found
121
+ #
122
+ def component(name)
123
+ @components.fetch(name.to_sym) do
124
+ raise Error, "Unknown component: #{name}"
125
+ end
126
+ end
127
+
128
+ # List registered components
129
+ #
130
+ # @return [Array<Symbol>]
131
+ #
132
+ def components
133
+ @components.keys
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Usage Examples
142
+
143
+ ### Basic View
144
+
145
+ ```ruby
146
+ class InvoiceReport < Pdf::View
147
+ title "INVOICE"
148
+ date :invoice_date # Uses real Date component now
149
+
150
+ paragraph :description
151
+
152
+ # Conditional rendering
153
+ render_if(:has_discount) do
154
+ alert :discount_info
155
+ end
156
+
157
+ section "Items" do
158
+ table :line_items, columns: [:name, :qty, :price]
159
+ end
160
+
161
+ section "Summary" do
162
+ paragraph :total_line
163
+ end
164
+
165
+ def invoice_date
166
+ data[:date] # Date component handles formatting
167
+ end
168
+
169
+ def description
170
+ "Invoice for #{data[:customer_name]}"
171
+ end
172
+
173
+ def has_discount
174
+ data[:discount].present?
175
+ end
176
+
177
+ def discount_info
178
+ { title: "Discount Applied!", description: data[:discount], color: "green" }
179
+ end
180
+
181
+ def line_items
182
+ data[:items]
183
+ end
184
+
185
+ def total_line
186
+ "Total: $#{data[:total]}"
187
+ end
188
+ end
189
+
190
+ # Generate
191
+ pdf = InvoiceReport.new(
192
+ date: Date.current,
193
+ customer_name: "Acme Corp",
194
+ items: [
195
+ { name: "Widget", qty: 5, price: "$10.00" },
196
+ { name: "Gadget", qty: 2, price: "$25.00" }
197
+ ],
198
+ total: "100.00"
199
+ ).to_pdf
200
+
201
+ File.binwrite("invoice.pdf", pdf)
202
+ ```
203
+
204
+ ### With Layout
205
+
206
+ ```ruby
207
+ class CompanyLayout < Pdf::Layout
208
+ header do
209
+ logo "logo.png", width: 100
210
+ qr_code data: :qr_url, size: 60, position: :right # Generates QR from data
211
+ context :header_lines
212
+ end
213
+
214
+ footer do
215
+ text :company_name, size: 8
216
+ page_number position: :right
217
+ end
218
+
219
+ margins top: 80, bottom: 60
220
+ end
221
+
222
+ class Report < Pdf::View
223
+ layout CompanyLayout
224
+
225
+ title :report_title
226
+ alert :summary
227
+
228
+ # Raw Prawn access when needed
229
+ raw do |pdf|
230
+ pdf.stroke_color "CCCCCC"
231
+ pdf.dash(2)
232
+ pdf.stroke_horizontal_line 0, pdf.bounds.width
233
+ pdf.undash
234
+ end
235
+
236
+ def report_title = data[:title]
237
+ def summary = data[:summary]
238
+ def header_lines = ["Company: #{data[:company]}", "Date: #{Date.current}"]
239
+ def company_name = data[:company]
240
+ def qr_url = "https://example.com/reports/#{data[:id]}"
241
+ end
242
+ ```
243
+
244
+ ### View Inheritance
245
+
246
+ Child views inherit parent's elements and can add more.
247
+
248
+ ```ruby
249
+ class BaseReport < Pdf::View
250
+ title "Company Report"
251
+ hr
252
+ spacer amount: 10
253
+ end
254
+
255
+ class SalesReport < BaseReport
256
+ # Inherits: title "Company Report", hr, spacer
257
+ subtitle "Sales Department"
258
+ table :sales_data, columns: [:product, :revenue]
259
+
260
+ def sales_data = data[:sales]
261
+ end
262
+
263
+ class FinanceReport < BaseReport
264
+ # Inherits: title "Company Report", hr, spacer
265
+ subtitle "Finance Department"
266
+ render_unless(:is_draft) { table :budget_data, columns: [:category, :amount] }
267
+
268
+ def budget_data = data[:budget]
269
+ def is_draft = data[:draft]
270
+ end
271
+ ```
272
+
273
+ ### Custom Component
274
+
275
+ ```ruby
276
+ class Badge < Pdf::Component
277
+ COLORS = {
278
+ "success" => "4CAF50",
279
+ "warning" => "FF9800",
280
+ "error" => "F44336"
281
+ }.freeze
282
+
283
+ def render(text, type: "success", **_options)
284
+ color = COLORS[type.to_s] || COLORS["success"]
285
+
286
+ bounding_box([0, cursor], width: 60, height: 20) do
287
+ @pdf.fill_color color
288
+ @pdf.fill_rounded_rectangle [0, 20], 60, 20, 4
289
+ @pdf.fill_color "FFFFFF"
290
+ @pdf.text_box text.to_s, at: [5, 16], width: 50, size: 10, align: :center
291
+ end
292
+
293
+ @pdf.fill_color "000000"
294
+ move_down 25
295
+ end
296
+ end
297
+
298
+ Pdf.register_component(:badge, Badge)
299
+
300
+ class StatusReport < Pdf::View
301
+ title "Status Report"
302
+ badge "LIVE", type: "success"
303
+ paragraph "All systems operational."
304
+ end
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Specs
310
+
311
+ ```ruby
312
+ # spec/pdf_spec.rb
313
+ RSpec.describe Pdf do
314
+ describe ".register_component" do
315
+ let(:custom_component) do
316
+ Class.new(Pdf::Component) do
317
+ def render(text) = @pdf.text(text)
318
+ end
319
+ end
320
+
321
+ after { described_class.instance_variable_get(:@components).delete(:custom) }
322
+
323
+ it "registers component" do
324
+ described_class.register_component(:custom, custom_component)
325
+ expect(described_class.component_registered?(:custom)).to be true
326
+ end
327
+
328
+ it "rejects non-Component classes" do
329
+ expect {
330
+ described_class.register_component(:bad, String)
331
+ }.to raise_error(ArgumentError)
332
+ end
333
+ end
334
+
335
+ describe ".component" do
336
+ it "returns built-in components" do
337
+ expect(described_class.component(:title)).to eq(Pdf::Components::Title)
338
+ end
339
+
340
+ it "returns date component" do
341
+ expect(described_class.component(:date)).to eq(Pdf::Components::Date)
342
+ end
343
+
344
+ it "returns spacer component" do
345
+ expect(described_class.component(:spacer)).to eq(Pdf::Components::Spacer)
346
+ end
347
+
348
+ it "raises for unknown" do
349
+ expect { described_class.component(:unknown) }.to raise_error(Pdf::Error)
350
+ end
351
+ end
352
+
353
+ describe ".components" do
354
+ it "lists all registered" do
355
+ expect(described_class.components).to include(:title, :table, :alert, :date, :spacer)
356
+ end
357
+
358
+ it "does not include page_number (handled by evaluator)" do
359
+ expect(described_class.components).not_to include(:page_number)
360
+ end
361
+ end
362
+
363
+ describe "Pdf::FileNotFoundError" do
364
+ it "is a subclass of Pdf::Error" do
365
+ expect(Pdf::FileNotFoundError.superclass).to eq(Pdf::Error)
366
+ end
367
+ end
368
+ end
369
+
370
+ # spec/integration_spec.rb
371
+ RSpec.describe "Integration" do
372
+ let(:layout_class) do
373
+ Class.new(Pdf::Layout) do
374
+ header do
375
+ context :header_lines
376
+ end
377
+
378
+ footer do
379
+ text :footer_text
380
+ page_number
381
+ end
382
+
383
+ margins top: 70, bottom: 60
384
+ end
385
+ end
386
+
387
+ let(:view_class) do
388
+ lc = layout_class
389
+ Class.new(Pdf::View) do
390
+ layout lc
391
+
392
+ title "TEST REPORT"
393
+ date :report_date
394
+ alert :status
395
+
396
+ render_if(:show_details) do
397
+ section "Data" do
398
+ table :items, columns: [:name, :value]
399
+ end
400
+ end
401
+
402
+ def report_date = Date.new(2024, 1, 15)
403
+ def status = { title: "OK", color: "green" }
404
+ def show_details = data[:show]
405
+ def items = data[:items]
406
+ def header_lines = ["Test Header"]
407
+ def footer_text = "Confidential"
408
+ end
409
+ end
410
+
411
+ it "generates valid PDF" do
412
+ pdf = view_class.new(items: [{ name: "A", value: "1" }], show: true).to_pdf
413
+ expect(pdf).to start_with("%PDF")
414
+ end
415
+
416
+ it "writes to file" do
417
+ path = "tmp/test.pdf"
418
+ view_class.new(items: [], show: false).to_file(path)
419
+ expect(File.exist?(path)).to be true
420
+ ensure
421
+ FileUtils.rm_f(path)
422
+ end
423
+
424
+ describe "view inheritance" do
425
+ let(:parent) { Class.new(Pdf::View) { title "Parent" } }
426
+ let(:child) { Class.new(parent) { subtitle "Child" } }
427
+
428
+ it "child generates PDF with both elements" do
429
+ pdf = child.new.to_pdf
430
+ expect(pdf).to start_with("%PDF")
431
+ expect(pdf).to include("Parent")
432
+ end
433
+ end
434
+ end
435
+ ```