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.
- checksums.yaml +4 -4
- data/.claude/execution/00-overview.md +121 -0
- data/.claude/execution/01-core.md +324 -0
- data/.claude/execution/02-renderer.md +237 -0
- data/.claude/execution/03-components.md +551 -0
- data/.claude/execution/04-builders.md +322 -0
- data/.claude/execution/05-view-layout.md +362 -0
- data/.claude/execution/06-evaluators.md +494 -0
- data/.claude/execution/07-entry-extensibility.md +435 -0
- data/.claude/execution/08-integration-tests.md +978 -0
- data/Rakefile +7 -3
- data/examples/01_basic_invoice.rb +139 -0
- data/examples/02_report_with_layout.rb +266 -0
- data/examples/03_inherited_views.rb +318 -0
- data/examples/04_conditional_content.rb +421 -0
- data/examples/05_custom_components.rb +442 -0
- data/examples/README.md +123 -0
- data/lib/pdf/blueprint.rb +50 -0
- data/lib/pdf/builders/content_builder.rb +96 -0
- data/lib/pdf/builders/footer_builder.rb +24 -0
- data/lib/pdf/builders/header_builder.rb +31 -0
- data/lib/pdf/component.rb +43 -0
- data/lib/pdf/components/alert.rb +42 -0
- data/lib/pdf/components/context.rb +16 -0
- data/lib/pdf/components/date.rb +28 -0
- data/lib/pdf/components/heading.rb +12 -0
- data/lib/pdf/components/hr.rb +12 -0
- data/lib/pdf/components/logo.rb +15 -0
- data/lib/pdf/components/paragraph.rb +12 -0
- data/lib/pdf/components/qr_code.rb +38 -0
- data/lib/pdf/components/spacer.rb +11 -0
- data/lib/pdf/components/span.rb +12 -0
- data/lib/pdf/components/subtitle.rb +12 -0
- data/lib/pdf/components/table.rb +48 -0
- data/lib/pdf/components/title.rb +12 -0
- data/lib/pdf/content_evaluator.rb +218 -0
- data/lib/pdf/dynamic_components.rb +17 -0
- data/lib/pdf/footer_evaluator.rb +66 -0
- data/lib/pdf/header_evaluator.rb +56 -0
- data/lib/pdf/layout.rb +61 -0
- data/lib/pdf/renderer.rb +153 -0
- data/lib/pdf/resolver.rb +36 -0
- data/lib/pdf/version.rb +1 -1
- data/lib/pdf/view.rb +113 -0
- data/lib/pdf.rb +74 -1
- 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)
|