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,421 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 04: Conditional Content
5
+ # Run: ruby examples/04_conditional_content.rb
6
+ #
7
+ # Demonstrates:
8
+ # - render_if for conditional sections
9
+ # - render_unless for inverse conditions
10
+ # - Complex boolean logic in conditions
11
+ # - Nested conditionals
12
+ # - Dynamic content based on data state
13
+ # - Different outputs from same view class
14
+
15
+ require_relative "../lib/pdf"
16
+
17
+ # --- Order Confirmation with Conditional Sections ---
18
+
19
+ class OrderConfirmation < Pdf::View
20
+ title "Order Confirmation"
21
+ date :order_date
22
+
23
+ spacer amount: 10
24
+
25
+ # Order status alert - color varies by status
26
+ alert :status_alert
27
+
28
+ hr
29
+
30
+ section "Order Details" do
31
+ paragraph :order_info
32
+ end
33
+
34
+ section "Items Ordered" do
35
+ table :order_items, columns: [
36
+ { key: :name, label: "Item" },
37
+ { key: :qty, label: "Qty" },
38
+ { key: :price, label: "Price" }
39
+ ]
40
+ end
41
+
42
+ # Only show discount section if there's a discount
43
+ render_if(:has_discount) do
44
+ section "Discount Applied" do
45
+ alert :discount_alert
46
+ end
47
+ end
48
+
49
+ # Show gift message only for gift orders
50
+ render_if(:is_gift) do
51
+ section "Gift Message" do
52
+ paragraph :gift_message_text
53
+ render_if(:gift_wrap_selected) do
54
+ span "Gift wrapping: Yes (+$5.00)", size: 9, color: "666666"
55
+ end
56
+ end
57
+ end
58
+
59
+ section "Order Summary" do
60
+ paragraph :subtotal_text
61
+ render_if(:has_discount) do
62
+ paragraph :discount_text
63
+ end
64
+ render_if(:has_shipping_cost) do
65
+ paragraph :shipping_text
66
+ end
67
+ render_unless(:free_shipping) do
68
+ span "(Free shipping on orders over $100)", size: 8, color: "888888"
69
+ end
70
+ paragraph :tax_text
71
+ heading :total_text, size: 14
72
+ end
73
+
74
+ hr
75
+
76
+ # Shipping info only for physical orders
77
+ render_unless(:digital_only) do
78
+ section "Shipping Information" do
79
+ paragraph :shipping_address
80
+ paragraph :estimated_delivery
81
+
82
+ render_if(:express_shipping) do
83
+ alert title: "Express Shipping Selected", description: "Priority handling - 1-2 business days", color: "blue"
84
+ end
85
+
86
+ render_if(:signature_required) do
87
+ span "Signature required upon delivery", size: 9
88
+ end
89
+ end
90
+ end
91
+
92
+ # Digital delivery info for digital orders
93
+ render_if(:has_digital_items) do
94
+ section "Digital Delivery" do
95
+ paragraph :digital_delivery_text
96
+ render_if(:requires_activation) do
97
+ alert title: "Activation Required", description: "Product keys will be emailed within 24 hours", color: "orange"
98
+ end
99
+ end
100
+ end
101
+
102
+ # Loyalty section only for members
103
+ render_if(:loyalty_member) do
104
+ section "Loyalty Rewards" do
105
+ alert :loyalty_alert
106
+ paragraph :points_earned_text
107
+ end
108
+ end
109
+
110
+ # Special instructions if any
111
+ render_if(:has_special_instructions) do
112
+ section "Special Instructions" do
113
+ paragraph :special_instructions
114
+ end
115
+ end
116
+
117
+ spacer amount: 20
118
+
119
+ span :footer_text, size: 8, color: "666666"
120
+
121
+ # --- Condition Methods ---
122
+
123
+ def has_discount
124
+ (data[:discount_percent] || 0) > 0 || (data[:discount_amount] || 0) > 0
125
+ end
126
+
127
+ def is_gift
128
+ data[:gift] == true
129
+ end
130
+
131
+ def gift_wrap_selected
132
+ data[:gift_wrap] == true
133
+ end
134
+
135
+ def has_shipping_cost
136
+ !digital_only && (data[:shipping_cost] || 0) > 0
137
+ end
138
+
139
+ def free_shipping
140
+ digital_only || subtotal >= 10000 # $100 in cents
141
+ end
142
+
143
+ def digital_only
144
+ data[:items]&.all? { |i| i[:digital] }
145
+ end
146
+
147
+ def has_digital_items
148
+ data[:items]&.any? { |i| i[:digital] }
149
+ end
150
+
151
+ def express_shipping
152
+ data[:shipping_method] == "express"
153
+ end
154
+
155
+ def signature_required
156
+ data[:signature_required] == true
157
+ end
158
+
159
+ def requires_activation
160
+ data[:items]&.any? { |i| i[:requires_activation] }
161
+ end
162
+
163
+ def loyalty_member
164
+ data[:loyalty_tier].to_s.length > 0
165
+ end
166
+
167
+ def has_special_instructions
168
+ data[:special_instructions].to_s.strip.length > 0
169
+ end
170
+
171
+ # --- Data Methods ---
172
+
173
+ def order_date
174
+ data[:date] || Date.today
175
+ end
176
+
177
+ def status_alert
178
+ status = data[:status] || "pending"
179
+ colors = { "confirmed" => "green", "processing" => "blue", "pending" => "orange", "on_hold" => "red" }
180
+ {
181
+ title: "Order Status: #{status.capitalize}",
182
+ description: status_description(status),
183
+ color: colors[status] || "blue"
184
+ }
185
+ end
186
+
187
+ def order_info
188
+ "Order #: #{data[:order_id]}\nCustomer: #{data[:customer_name]}\nEmail: #{data[:email]}"
189
+ end
190
+
191
+ def order_items
192
+ data[:items].map do |item|
193
+ {
194
+ name: item[:name] + (item[:digital] ? " (Digital)" : ""),
195
+ qty: item[:quantity],
196
+ price: format_currency(item[:price_cents] * item[:quantity])
197
+ }
198
+ end
199
+ end
200
+
201
+ def discount_alert
202
+ if data[:discount_percent]
203
+ { title: "#{data[:discount_percent]}% Off Applied!", description: "Code: #{data[:discount_code]}", color: "green" }
204
+ else
205
+ { title: "Discount: #{format_currency(data[:discount_amount])}", description: "Code: #{data[:discount_code]}", color: "green" }
206
+ end
207
+ end
208
+
209
+ def gift_message_text
210
+ "\"#{data[:gift_message]}\"\n- #{data[:gift_from] || 'Anonymous'}"
211
+ end
212
+
213
+ def subtotal_text
214
+ "Subtotal: #{format_currency(subtotal)}"
215
+ end
216
+
217
+ def discount_text
218
+ amount = data[:discount_percent] ? (subtotal * data[:discount_percent] / 100.0).round : data[:discount_amount]
219
+ "Discount: -#{format_currency(amount)}"
220
+ end
221
+
222
+ def shipping_text
223
+ "Shipping: #{format_currency(data[:shipping_cost])}"
224
+ end
225
+
226
+ def tax_text
227
+ "Tax: #{format_currency(data[:tax_cents] || 0)}"
228
+ end
229
+
230
+ def total_text
231
+ "TOTAL: #{format_currency(calculate_total)}"
232
+ end
233
+
234
+ def shipping_address
235
+ addr = data[:shipping_address]
236
+ "#{addr[:name]}\n#{addr[:street]}\n#{addr[:city]}, #{addr[:state]} #{addr[:zip]}\n#{addr[:country]}"
237
+ end
238
+
239
+ def estimated_delivery
240
+ days = express_shipping ? "1-2" : "5-7"
241
+ "Estimated Delivery: #{days} business days"
242
+ end
243
+
244
+ def digital_delivery_text
245
+ "Download links will be sent to: #{data[:email]}"
246
+ end
247
+
248
+ def loyalty_alert
249
+ tier = data[:loyalty_tier]
250
+ { title: "#{tier.capitalize} Member", description: "Thank you for being a valued customer!", color: "blue" }
251
+ end
252
+
253
+ def points_earned_text
254
+ points = (calculate_total / 100.0).round
255
+ "Points earned on this order: #{points}\nTotal points balance: #{data[:loyalty_points] || 0}"
256
+ end
257
+
258
+ def special_instructions
259
+ data[:special_instructions]
260
+ end
261
+
262
+ def footer_text
263
+ "Thank you for your order! Questions? Contact support@example.com or call 1-800-EXAMPLE"
264
+ end
265
+
266
+ private
267
+
268
+ def subtotal
269
+ data[:items].sum { |i| i[:price_cents] * i[:quantity] }
270
+ end
271
+
272
+ def calculate_total
273
+ total = subtotal
274
+ total -= (data[:discount_percent] ? (subtotal * data[:discount_percent] / 100.0).round : (data[:discount_amount] || 0))
275
+ total += (data[:shipping_cost] || 0) unless digital_only || free_shipping
276
+ total += (data[:tax_cents] || 0)
277
+ total += 500 if is_gift && gift_wrap_selected # gift wrap fee
278
+ total
279
+ end
280
+
281
+ def status_description(status)
282
+ {
283
+ "confirmed" => "Your order has been confirmed and will be processed shortly.",
284
+ "processing" => "Your order is being prepared for shipment.",
285
+ "pending" => "Your order is awaiting payment confirmation.",
286
+ "on_hold" => "Your order is on hold. Please contact support."
287
+ }[status] || "Status update pending."
288
+ end
289
+
290
+ def format_currency(cents)
291
+ "$#{'%.2f' % (cents / 100.0)}"
292
+ end
293
+ end
294
+
295
+ # --- Generate Multiple Variations ---
296
+
297
+ puts "Generating conditional content examples...\n\n"
298
+
299
+ # 1. Simple digital order (minimal sections)
300
+ digital_order = {
301
+ order_id: "ORD-001-DIG",
302
+ date: Date.today,
303
+ status: "confirmed",
304
+ customer_name: "Jane Doe",
305
+ email: "jane@example.com",
306
+ items: [
307
+ { name: "E-Book: Ruby Mastery", quantity: 1, price_cents: 2999, digital: true },
308
+ { name: "Video Course: Rails Pro", quantity: 1, price_cents: 9999, digital: true, requires_activation: true }
309
+ ],
310
+ tax_cents: 1040
311
+ }
312
+
313
+ digital_path = "/tmp/order_digital.pdf"
314
+ OrderConfirmation.new(digital_order).to_file(digital_path)
315
+ puts "Generated Digital Order: #{digital_path}"
316
+
317
+ # 2. Gift order with discount and express shipping
318
+ gift_order = {
319
+ order_id: "ORD-002-GIFT",
320
+ date: Date.today,
321
+ status: "processing",
322
+ customer_name: "John Smith",
323
+ email: "john@example.com",
324
+ gift: true,
325
+ gift_wrap: true,
326
+ gift_message: "Happy Birthday! Hope you enjoy this!",
327
+ gift_from: "Your Secret Santa",
328
+ discount_percent: 15,
329
+ discount_code: "BDAY15",
330
+ shipping_method: "express",
331
+ signature_required: true,
332
+ shipping_cost: 1499,
333
+ tax_cents: 890,
334
+ shipping_address: {
335
+ name: "Mary Johnson",
336
+ street: "456 Oak Avenue, Apt 7B",
337
+ city: "Boston",
338
+ state: "MA",
339
+ zip: "02101",
340
+ country: "USA"
341
+ },
342
+ items: [
343
+ { name: "Wireless Headphones", quantity: 1, price_cents: 7999 },
344
+ { name: "Phone Case", quantity: 1, price_cents: 2499 }
345
+ ],
346
+ loyalty_tier: "gold",
347
+ loyalty_points: 1250
348
+ }
349
+
350
+ gift_path = "/tmp/order_gift.pdf"
351
+ OrderConfirmation.new(gift_order).to_file(gift_path)
352
+ puts "Generated Gift Order: #{gift_path}"
353
+
354
+ # 3. Mixed order (physical + digital) with special instructions
355
+ mixed_order = {
356
+ order_id: "ORD-003-MIX",
357
+ date: Date.today,
358
+ status: "confirmed",
359
+ customer_name: "Alex Rivera",
360
+ email: "alex@example.com",
361
+ discount_amount: 1000,
362
+ discount_code: "SAVE10",
363
+ shipping_cost: 599,
364
+ tax_cents: 1234,
365
+ shipping_address: {
366
+ name: "Alex Rivera",
367
+ street: "789 Pine Street",
368
+ city: "San Francisco",
369
+ state: "CA",
370
+ zip: "94102",
371
+ country: "USA"
372
+ },
373
+ items: [
374
+ { name: "Programming Keyboard", quantity: 1, price_cents: 14999 },
375
+ { name: "USB-C Hub", quantity: 2, price_cents: 3999 },
376
+ { name: "IDE License Key", quantity: 1, price_cents: 9999, digital: true, requires_activation: true }
377
+ ],
378
+ loyalty_tier: "silver",
379
+ loyalty_points: 450,
380
+ special_instructions: "Please leave package at the back door if no one is home. " \
381
+ "Ring doorbell twice. Fragile items - handle with care!"
382
+ }
383
+
384
+ mixed_path = "/tmp/order_mixed.pdf"
385
+ OrderConfirmation.new(mixed_order).to_file(mixed_path)
386
+ puts "Generated Mixed Order: #{mixed_path}"
387
+
388
+ # 4. On-hold order (shows warning state)
389
+ hold_order = {
390
+ order_id: "ORD-004-HOLD",
391
+ date: Date.today - 2,
392
+ status: "on_hold",
393
+ customer_name: "Pat Wilson",
394
+ email: "pat@example.com",
395
+ shipping_cost: 0,
396
+ tax_cents: 500,
397
+ shipping_address: {
398
+ name: "Pat Wilson",
399
+ street: "321 Elm Road",
400
+ city: "Chicago",
401
+ state: "IL",
402
+ zip: "60601",
403
+ country: "USA"
404
+ },
405
+ items: [
406
+ { name: "Basic Widget", quantity: 3, price_cents: 1999 }
407
+ ]
408
+ }
409
+
410
+ hold_path = "/tmp/order_on_hold.pdf"
411
+ OrderConfirmation.new(hold_order).to_file(hold_path)
412
+ puts "Generated On-Hold Order: #{hold_path}"
413
+
414
+ puts "\nOpening all four order variations..."
415
+ system("open", digital_path)
416
+ sleep 0.3
417
+ system("open", gift_path)
418
+ sleep 0.3
419
+ system("open", mixed_path)
420
+ sleep 0.3
421
+ system("open", hold_path)