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
data/Rakefile CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
4
+ require "rake/testtask"
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
7
11
 
8
12
  require "rubocop/rake_task"
9
13
 
10
14
  RuboCop::RakeTask.new
11
15
 
12
- task default: %i[spec rubocop]
16
+ task default: %i[test rubocop]
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 01: Basic Invoice
5
+ # Run: ruby examples/01_basic_invoice.rb
6
+ #
7
+ # Demonstrates:
8
+ # - Basic view structure
9
+ # - Title, subtitle, date components
10
+ # - Table with columns
11
+ # - Sections
12
+ # - Data binding via symbols
13
+ # - Method-based computed values
14
+
15
+ require_relative "../lib/pdf"
16
+
17
+ class Invoice < Pdf::View
18
+ title "INVOICE"
19
+ date :invoice_date, format: "%B %d, %Y"
20
+
21
+ spacer amount: 10
22
+
23
+ paragraph :billing_info
24
+
25
+ hr
26
+
27
+ section "Order Items" do
28
+ table :line_items, columns: [
29
+ { key: :sku, label: "SKU" },
30
+ { key: :description, label: "Description" },
31
+ { key: :quantity, label: "Qty" },
32
+ { key: :unit_price, label: "Unit Price" },
33
+ { key: :total, label: "Total" }
34
+ ], widths: ["15%", "40%", "10%", "17.5%", "17.5%"]
35
+ end
36
+
37
+ spacer amount: 15
38
+
39
+ section "Summary" do
40
+ paragraph :subtotal_line
41
+ paragraph :tax_line
42
+ heading :total_line, size: 14
43
+ end
44
+
45
+ spacer amount: 20
46
+
47
+ alert :payment_info
48
+
49
+ hr
50
+
51
+ span :footer_note, size: 8, color: "666666"
52
+
53
+ # Data accessors
54
+
55
+ def invoice_date
56
+ data[:date] || Date.today
57
+ end
58
+
59
+ def billing_info
60
+ <<~INFO
61
+ Invoice #: #{data[:invoice_number]}
62
+ Customer: #{data[:customer][:name]}
63
+ Address: #{data[:customer][:address]}
64
+ Email: #{data[:customer][:email]}
65
+ INFO
66
+ end
67
+
68
+ def line_items
69
+ data[:items].map do |item|
70
+ item.merge(total: format_currency(item[:quantity] * item[:unit_price_cents]))
71
+ end
72
+ end
73
+
74
+ def subtotal_line
75
+ "Subtotal: #{format_currency(subtotal_cents)}"
76
+ end
77
+
78
+ def tax_line
79
+ "Tax (#{data[:tax_rate]}%): #{format_currency(tax_cents)}"
80
+ end
81
+
82
+ def total_line
83
+ "TOTAL DUE: #{format_currency(subtotal_cents + tax_cents)}"
84
+ end
85
+
86
+ def payment_info
87
+ {
88
+ title: "Payment Due: #{(data[:date] + 30).strftime('%B %d, %Y')}",
89
+ description: "Please remit payment to: #{data[:payment_details]}",
90
+ color: "blue"
91
+ }
92
+ end
93
+
94
+ def footer_note
95
+ "Thank you for your business! Questions? Contact us at billing@example.com"
96
+ end
97
+
98
+ private
99
+
100
+ def subtotal_cents
101
+ data[:items].sum { |i| i[:quantity] * i[:unit_price_cents] }
102
+ end
103
+
104
+ def tax_cents
105
+ (subtotal_cents * data[:tax_rate] / 100.0).round
106
+ end
107
+
108
+ def format_currency(cents)
109
+ "$#{'%.2f' % (cents / 100.0)}"
110
+ end
111
+ end
112
+
113
+ # --- Generate the invoice ---
114
+
115
+ invoice_data = {
116
+ invoice_number: "INV-2024-0042",
117
+ date: Date.new(2024, 12, 15),
118
+ tax_rate: 8.5,
119
+ payment_details: "Acme Corp, Account #12345678",
120
+ customer: {
121
+ name: "John Smith",
122
+ address: "123 Main Street, Anytown, ST 12345",
123
+ email: "john.smith@example.com"
124
+ },
125
+ items: [
126
+ { sku: "WDG-001", description: "Premium Widget (Blue)", quantity: 5, unit_price_cents: 2499 },
127
+ { sku: "WDG-002", description: "Deluxe Widget (Red)", quantity: 3, unit_price_cents: 3499 },
128
+ { sku: "GDG-001", description: "Standard Gadget", quantity: 10, unit_price_cents: 999 },
129
+ { sku: "ACC-001", description: "Widget Mounting Kit", quantity: 2, unit_price_cents: 1599 },
130
+ { sku: "SVC-001", description: "Installation Service", quantity: 1, unit_price_cents: 7500 }
131
+ ]
132
+ }
133
+
134
+ # Generate and open
135
+ output_path = "/tmp/invoice_example.pdf"
136
+ Invoice.new(invoice_data).to_file(output_path)
137
+
138
+ puts "Generated: #{output_path}"
139
+ system("open", output_path)
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 02: Report with Layout
5
+ # Run: ruby examples/02_report_with_layout.rb
6
+ #
7
+ # Demonstrates:
8
+ # - Custom layout with header and footer
9
+ # - QR code generation from data
10
+ # - Context lines in header
11
+ # - Page numbers
12
+ # - Multi-section report
13
+ # - Alerts with different colors
14
+ # - Tables with data transformation
15
+ # - Raw Prawn access for custom drawing
16
+
17
+ require_relative "../lib/pdf"
18
+
19
+ # --- Layout Definition ---
20
+
21
+ class CompanyReportLayout < Pdf::Layout
22
+ header do
23
+ qr_code data: :document_url, size: 50, position: :right
24
+ context :header_context, label: "REPORT INFO"
25
+ end
26
+
27
+ footer do
28
+ text :footer_disclaimer, size: 7
29
+ page_number position: :right, format: "Page %<page>d of %<total>d"
30
+ end
31
+
32
+ margins top: 90, bottom: 70, left: 50, right: 50
33
+ end
34
+
35
+ # --- Report View ---
36
+
37
+ class QuarterlyReport < Pdf::View
38
+ layout CompanyReportLayout
39
+
40
+ title :report_title, size: 22
41
+ subtitle :report_period
42
+ date :generated_at, format: "Generated: %B %d, %Y at %H:%M"
43
+
44
+ spacer amount: 15
45
+
46
+ alert :executive_summary
47
+
48
+ hr
49
+
50
+ section "Revenue Breakdown" do
51
+ table :revenue_data, columns: [
52
+ { key: :category, label: "Category" },
53
+ { key: :q1, label: "Q1" },
54
+ { key: :q2, label: "Q2" },
55
+ { key: :q3, label: "Q3" },
56
+ { key: :q4, label: "Q4" },
57
+ { key: :total, label: "Total" }
58
+ ], widths: ["25%", "15%", "15%", "15%", "15%", "15%"]
59
+ end
60
+
61
+ spacer amount: 10
62
+
63
+ # Custom chart placeholder using raw Prawn
64
+ raw do |pdf|
65
+ pdf.text "Revenue Trend (Visual)", size: 12, style: :bold
66
+ pdf.move_down 10
67
+
68
+ # Draw a simple bar chart
69
+ start_x = 50
70
+ bar_width = 60
71
+ max_height = 100
72
+ values = [65, 78, 82, 95]
73
+ labels = ["Q1", "Q2", "Q3", "Q4"]
74
+ colors = ["4CAF50", "2196F3", "FF9800", "9C27B0"]
75
+
76
+ values.each_with_index do |val, i|
77
+ x = start_x + (i * (bar_width + 20))
78
+ height = (val / 100.0) * max_height
79
+
80
+ pdf.fill_color colors[i]
81
+ pdf.fill_rectangle [x, pdf.cursor], bar_width, height
82
+
83
+ pdf.fill_color "333333"
84
+ pdf.draw_text labels[i], at: [x + 20, pdf.cursor - height - 15], size: 10
85
+ pdf.draw_text "#{val}%", at: [x + 18, pdf.cursor - height + 10], size: 8
86
+ end
87
+
88
+ pdf.fill_color "000000"
89
+ pdf.move_down max_height + 30
90
+ end
91
+
92
+ hr
93
+
94
+ section "Key Performance Indicators" do
95
+ each(:kpis) do |kpi|
96
+ alert kpi
97
+ end
98
+ end
99
+
100
+ section "Regional Performance" do
101
+ table :regional_data, columns: [
102
+ { key: :region, label: "Region" },
103
+ { key: :revenue, label: "Revenue" },
104
+ { key: :growth, label: "YoY Growth" },
105
+ { key: :status, label: "Status" }
106
+ ]
107
+ end
108
+
109
+ spacer amount: 15
110
+
111
+ section "Notes & Observations" do
112
+ each(:observations) do |obs|
113
+ paragraph obs
114
+ end
115
+ end
116
+
117
+ page_break
118
+
119
+ section "Appendix: Detailed Metrics" do
120
+ table :detailed_metrics, columns: [:metric, :value, :target, :variance]
121
+ end
122
+
123
+ # --- Data Methods ---
124
+
125
+ def report_title
126
+ "#{data[:company]} Quarterly Report"
127
+ end
128
+
129
+ def report_period
130
+ "#{data[:quarter]} #{data[:year]}"
131
+ end
132
+
133
+ def generated_at
134
+ Time.now
135
+ end
136
+
137
+ def header_context
138
+ [
139
+ "Company: #{data[:company]}",
140
+ "Period: #{data[:quarter]} #{data[:year]}",
141
+ "Confidentiality: #{data[:confidentiality]}"
142
+ ]
143
+ end
144
+
145
+ def document_url
146
+ "https://reports.example.com/#{data[:report_id]}"
147
+ end
148
+
149
+ def footer_disclaimer
150
+ "#{data[:company]} - Confidential. Distribution restricted to authorized personnel only."
151
+ end
152
+
153
+ def executive_summary
154
+ {
155
+ title: "Executive Summary",
156
+ description: data[:summary],
157
+ color: "blue"
158
+ }
159
+ end
160
+
161
+ def revenue_data
162
+ data[:revenue].map do |row|
163
+ row.merge(total: format_currency(row.values_at(:q1, :q2, :q3, :q4).map { |v| parse_currency(v) }.sum))
164
+ end
165
+ end
166
+
167
+ def kpis
168
+ data[:kpis].map do |kpi|
169
+ color = case kpi[:status]
170
+ when :good then "green"
171
+ when :warning then "orange"
172
+ when :critical then "red"
173
+ else "blue"
174
+ end
175
+ {
176
+ title: "#{kpi[:name]}: #{kpi[:value]}",
177
+ description: "Target: #{kpi[:target]} | #{kpi[:trend]}",
178
+ color: color
179
+ }
180
+ end
181
+ end
182
+
183
+ def regional_data
184
+ data[:regions]
185
+ end
186
+
187
+ def observations
188
+ data[:observations]
189
+ end
190
+
191
+ def detailed_metrics
192
+ data[:metrics]
193
+ end
194
+
195
+ private
196
+
197
+ def format_currency(cents)
198
+ "$#{cents.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
199
+ end
200
+
201
+ def parse_currency(str)
202
+ str.to_s.gsub(/[$,]/, "").to_i
203
+ end
204
+ end
205
+
206
+ # --- Generate the report ---
207
+
208
+ report_data = {
209
+ company: "Acme Corporation",
210
+ quarter: "Q4",
211
+ year: 2024,
212
+ report_id: "RPT-2024-Q4-001",
213
+ confidentiality: "Internal Use Only",
214
+
215
+ summary: "Q4 2024 showed strong performance across all regions with 18% YoY revenue growth. " \
216
+ "The APAC region exceeded targets by 25%, while EMEA stabilized after Q3 restructuring. " \
217
+ "Customer acquisition costs decreased by 12% due to improved marketing efficiency.",
218
+
219
+ revenue: [
220
+ { category: "Product Sales", q1: "$1.2M", q2: "$1.4M", q3: "$1.5M", q4: "$1.8M" },
221
+ { category: "Services", q1: "$800K", q2: "$850K", q3: "$920K", q4: "$1.1M" },
222
+ { category: "Subscriptions", q1: "$2.1M", q2: "$2.3M", q3: "$2.5M", q4: "$2.8M" },
223
+ { category: "Licensing", q1: "$400K", q2: "$420K", q3: "$450K", q4: "$500K" }
224
+ ],
225
+
226
+ kpis: [
227
+ { name: "Revenue Growth", value: "+18%", target: "+15%", trend: "Exceeding target", status: :good },
228
+ { name: "Customer Retention", value: "94%", target: "95%", trend: "Slightly below", status: :warning },
229
+ { name: "NPS Score", value: "72", target: "70", trend: "Improving", status: :good },
230
+ { name: "CAC", value: "$142", target: "$150", trend: "12% reduction", status: :good },
231
+ { name: "Churn Rate", value: "2.8%", target: "2.5%", trend: "Needs attention", status: :warning }
232
+ ],
233
+
234
+ regions: [
235
+ { region: "North America", revenue: "$3.2M", growth: "+15%", status: "On Track" },
236
+ { region: "EMEA", revenue: "$1.8M", growth: "+8%", status: "Recovering" },
237
+ { region: "APAC", revenue: "$1.5M", growth: "+32%", status: "Exceeding" },
238
+ { region: "LATAM", revenue: "$0.7M", growth: "+22%", status: "Growing" }
239
+ ],
240
+
241
+ observations: [
242
+ "Product launch in October drove significant Q4 growth in North America.",
243
+ "APAC expansion strategy showing strong ROI, consider accelerating.",
244
+ "EMEA restructuring complete, expect normalized growth in Q1 2025.",
245
+ "Customer feedback indicates high demand for enterprise features.",
246
+ "Recommend increasing R&D budget allocation for H1 2025."
247
+ ],
248
+
249
+ metrics: [
250
+ { metric: "MRR", value: "$580K", target: "$550K", variance: "+5.5%" },
251
+ { metric: "ARR", value: "$6.96M", target: "$6.6M", variance: "+5.5%" },
252
+ { metric: "Active Users", value: "45,200", target: "42,000", variance: "+7.6%" },
253
+ { metric: "Avg. Deal Size", value: "$12,400", target: "$11,000", variance: "+12.7%" },
254
+ { metric: "Sales Cycle", value: "34 days", target: "40 days", variance: "-15%" },
255
+ { metric: "Support Tickets", value: "1,240", target: "1,500", variance: "-17.3%" },
256
+ { metric: "Resolution Time", value: "4.2 hrs", target: "6 hrs", variance: "-30%" },
257
+ { metric: "Employee Count", value: "128", target: "125", variance: "+2.4%" }
258
+ ]
259
+ }
260
+
261
+ # Generate and open
262
+ output_path = "/tmp/quarterly_report_example.pdf"
263
+ QuarterlyReport.new(report_data).to_file(output_path)
264
+
265
+ puts "Generated: #{output_path}"
266
+ system("open", output_path)