webtranslateit-payday 1.7.4 → 2.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 655aa13a2a0c729008a5bce34e5e8de25b5fafd1f3e571995addf916413f74f8
4
- data.tar.gz: 7d78b88d6886fbfceb45377b0c25c3cdbc6b3886a33003460cc2f68490004f0c
3
+ metadata.gz: 4cb98acc78809a09e4945f8d8525aee1bbafd3ed17a45234be6fb0a8836f3135
4
+ data.tar.gz: b3379b263fb55f5d7fdb8ec48d66924e0f0b1110325b43e15bbd0a764fbc6154
5
5
  SHA512:
6
- metadata.gz: 36a5311dd8d6858c70a8b00a47bc675beb3de3c72827e0b5eb330f7113566051723dfa1f9cdbe636d745363bd02b3458e861803c2d0f481e7269805ae0c2d95e
7
- data.tar.gz: 5342f87b7e7d2568bc50141463557e8929ae1f678cdbdfb7776cdeb560fdaf215bd8b4f7b4fa8588b861befe38330225777a94783af558434ab671f3440cb2f7
6
+ metadata.gz: c0473911f660484293361d1fbfb7020efed4614c06dd89b000fc7995f90e52b995c763781f175609578fd42090923ed9969baab0d4bffb85140f887356964690
7
+ data.tar.gz: 0a401edbb42eb073bf35602caaf6c2b7b762450dff19a04f2a8ee4d3ae8aca0ecf2ceab4485300a0647a2005b22116edd2cd1eba0be60a07117f5d44e5637b39
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.1.0 (2026-08-27)
4
+
5
+ * Add `Payday::Appendix`, for compiling pages of your own onto the end of an invoice. `render_pdf`, `render_pdf_to_file`, `PdfRenderer.render` and `PdfRenderer.render_to_file` all take an `appendix:` keyword; every signature stays backward compatible and an invoice rendered without one is byte-for-byte what it was.
6
+
7
+ The appendix carries Typst `source`, plus `inputs` that reach it as sys_inputs JSON and `dependencies` it can resolve by name. The source is concatenated onto Payday's template rather than compiled separately, so the appended pages keep the invoice's page setup and the numbering footer counts them: a one-page invoice with an appendix numbers "1 / 2" and "2 / 2". Merging a second PDF in could do neither.
8
+
9
+ Data belongs in `inputs`, where Typst treats it as literal text. Interpolating invoice data into `source` would give it the run of the document, which is the same rule the invoice template already follows.
10
+
11
+ An appendix that reuses a name the invoice has taken — the `invoice` input, or the logo and QR code files — raises `ArgumentError` instead of replacing it.
12
+
13
+ ## 2.0.0 (2026-08-26)
14
+
15
+ **Breaking:** Payday now renders with [Typst](https://typst.app) instead of Prawn. The `prawn`, `prawn-table` and `prawn-svg` dependencies are gone, replaced by the single Apache-2.0 licensed `typst` gem, which ships precompiled native builds for macOS and Linux on both x86_64 and arm64.
16
+
17
+ The motivation was `prawn-table`, which has had no release since 2015 while the renderer depended on it for every table.
18
+
19
+ * The public API is unchanged. `Invoiceable#render_pdf`, `#render_pdf_to_file`, `PdfRenderer.render`, `PdfRenderer.render_to_file` and every `Payday::Config` accessor keep their signatures, and rendered invoices keep their existing layout.
20
+ * Prawn is no longer loaded for you. If your application relied on Payday requiring Prawn, require it yourself.
21
+ * Layout now lives in `lib/payday/templates/invoice.typ`, and invoice data is turned into a plain Hash by the new `Payday::InvoicePresenter`.
22
+ * The `inline_format` markup Payday accepted in notes and line item descriptions still works. `Payday::Markup` converts `<b>`, `<i>`, `<u>`, `<strikethrough>`, `<sub>`, `<sup>`, `<font size>`, `<color rgb>`, `<link href>` and `<br>` into styled runs.
23
+ * Invoice data reaches the template only as JSON, where Typst treats every string as literal text, so customer-supplied fields can no longer affect the layout.
24
+ * Rendered PDFs are now byte-for-byte reproducible for the same input.
25
+ * QR codes are drawn as vector SVG rather than a scaled 200px PNG, so they stay sharp in print, and the three finder patterns are drawn as rounded frames. The matrix still comes straight from RQRCode: the payload and its error correction level are untouched, and the rendered code is module-for-module identical to what the encoder produces.
26
+ * `Config#company_details` has any per-line indentation stripped. Prawn rendered each line as its own table cell, which trimmed leading whitespace, so a value written as an indented heredoc used to look fine and would otherwise have started rendering with its indentation.
27
+ * Logos still accept PNG, JPEG and SVG, and a logo given as a bare path keeps its natural size. Only the `{filename:, size:}` form constrains the dimensions.
28
+
29
+ Two packaging bugs are fixed along the way, both of which predate this release:
30
+
31
+ * Payday's own translations now load. The load path was resolved against the working directory, so unless a process happened to run from the gem's root the locales never loaded at all, and `I18n.t` raised `InvalidLocale` rather than falling back to the built-in English defaults. The German, Spanish, French, Dutch and Chinese translations that ship with the gem have effectively never been used.
32
+ * The default `invoice_logo` points inside the gem. It used to reference a file under `spec/`, which only resolved because the gemspec shipped the whole test suite.
33
+
34
+ The packaged gem now carries only `lib`, `config/locales`, `fonts` and the docs, which takes it from 563K to 277K.
35
+
36
+ Invoices keep the layout the prawn renderer produced. Page margins, table row heights, block spacing and the position of the status stamp were all measured against the previous output, and four of the five reference invoices in the test suite now match it within a single point at every word.
37
+
3
38
  ## 1.7.4 (2026-08-13)
4
39
 
5
40
  * Pass `enable_web_requests: false` when rendering SVG logos. Logos are read from local files, so nothing needs fetching over the network. This silences prawn-svg's deprecation warning and keeps behaviour stable when prawn-svg 1.0 flips the default to `false`.
data/README.md CHANGED
@@ -2,7 +2,9 @@ Payday!
2
2
  ===
3
3
  Payday is a library for rendering invoices to pdfs.
4
4
 
5
- We use payday intensively at [WebTranslateIt.com](https://webtranslateit.com) to generate customer’s invoices and receipts. When we upgraded our app to ruby 2.7 this gem failed in many ways and the original source code by Alan Johnson was nowhere to be found on GitHub. So we took the source code out of [rubygems.org](https://rubygems.org) and created this repo. We’ve since added a few new features and fixes, which can be seen [in the changelog](https://github.com/webtranslateit/payday/blob/main/CHANGELOG.md).
5
+ We use payday intensively at [WebTranslateIt.com](https://webtranslateit.com) and [Trackberry](https://trackberry.com) to generate customer’s invoices and receipts. When we upgraded our app to ruby 2.7 this gem failed in many ways and the original source code by Alan Johnson was nowhere to be found on GitHub. So we took the source code out of [rubygems.org](https://rubygems.org) and created this repo. We’ve since added a few new features and fixes, which can be seen [in the changelog](https://github.com/webtranslateit/payday/blob/main/CHANGELOG.md).
6
+
7
+ Invoices can now carry a QR code, which a growing number of countries expect for tax compliance, and as of 2.0 the PDFs are rendered with [Typst](https://typst.app) rather than Prawn.
6
8
 
7
9
  Here’s an [example PDF Invoice](https://github.com/webtranslateit/payday/raw/main/spec/assets/testing_predefined_amount.pdf) generated with our fork of payday.
8
10
 
@@ -53,6 +55,15 @@ Payday::Config.default.company_name = "Awesome Corp"
53
55
  Payday::Config.default.company_details = "10 This Way\nManhattan, NY 10001\n800-111-2222\nawesome@awesomecorp.com"
54
56
  ```
55
57
 
58
+ The logo can be a PNG, a JPEG or an SVG, and is drawn at its natural size. Pass a hash to
59
+ constrain it:
60
+
61
+ ``` ruby
62
+ Payday::Config.default.invoice_logo = {filename: "logo.svg", size: "200x50"}
63
+ ```
64
+
65
+ `page_size` accepts `"LETTER"`, `"A4"` and `"LEGAL"`.
66
+
56
67
  QR Code Support
57
68
  ===
58
69
  Invoices can include QR codes for verification purposes, useful for tax compliance requirements in various countries.
@@ -67,7 +78,9 @@ invoice = Payday::Invoice.new(
67
78
  )
68
79
  ```
69
80
 
70
- QR codes are automatically rendered below the notes section.
81
+ QR codes are automatically rendered below the notes section, as vector SVG so they stay
82
+ sharp in print. The code itself comes straight from [rqrcode](https://github.com/whomwah/rqrcode),
83
+ so the payload and its error correction level are exactly what that gem produces.
71
84
 
72
85
  Using Payday with ActiveRecord Objects (or any other objects, for that matter)
73
86
  ===
@@ -102,6 +115,35 @@ end
102
115
 
103
116
  Be sure to restart your server after you edit the mime_types initializer. The updated setting won't take effect until you do.
104
117
 
118
+ Appending your own pages
119
+ ===
120
+ Pass a `Payday::Appendix` to add pages of your own to the end of an invoice — a per-item breakdown, a delivery note, terms and conditions:
121
+
122
+ ``` ruby
123
+ appendix = Payday::Appendix.new(
124
+ source: File.read('appendix.typ'),
125
+ inputs: {'shipments' => shipments.to_json},
126
+ dependencies: {'chart.svg' => File.binread('chart.svg')}
127
+ )
128
+
129
+ invoice.render_pdf(appendix: appendix)
130
+ ```
131
+
132
+ `source` is [Typst](https://typst.app) markup appended to Payday's own template, so it is compiled as part of the invoice rather than as a second PDF you would then have to merge in. Two things follow from that: your pages inherit the invoice's page setup and margins, and the page counter runs across the whole document, so the numbering footer counts your pages too. A one-page invoice with one appended page comes out numbered "1 / 2" and "2 / 2".
133
+
134
+ `inputs` reach the template as [sys_inputs](https://typst.app/docs/reference/foundations/sys/), read with `json(bytes(sys.inputs.shipments))`. `dependencies` are files the source can reach by name, with `image("chart.svg")` and the like. Reusing a name the invoice has already taken — the `invoice` input, or the logo and QR code files — raises an `ArgumentError` rather than quietly replacing it.
135
+
136
+ **Pass your data through `inputs`, never by interpolating it into `source`.** Typst treats a string arriving through `sys.inputs` as literal text, which is what stops a customer-supplied field from affecting the layout. `source` is code: interpolating invoice data into it hands anyone who can set a field on an invoice a way to write Typst.
137
+
138
+ An appendix that reads its data and prints a heading:
139
+
140
+ ``` typst
141
+ #let a = json(bytes(sys.inputs.shipments))
142
+
143
+ #pagebreak()
144
+ #text(size: 16pt, weight: "bold")[#a.title]
145
+ ```
146
+
105
147
  I18n
106
148
  ===
107
149
  Payday uses the i18n gem to provide support for custom labels and internationalized applications. You can change the default labels by adding a YAML file in the `config/locales` directory of your Rails app. Here are the default labels you can customize:
@@ -166,7 +208,9 @@ Here's what we're planning on working on with Payday in the near future:
166
208
 
167
209
  Acknowledgements
168
210
  ===
169
- This wouldn't be possible without the amazing [Prawn](http://prawn.majesticseacreature.com) gem and the team behind it.
211
+ Payday renders with [Typst](https://typst.app) and its [Ruby binding](https://github.com/actsasflinn/typst-rb).
212
+
213
+ For its first decade this gem was built on [Prawn](https://prawnpdf.org), and it would not exist without that project and the team behind it.
170
214
 
171
215
  License
172
216
  ===
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Payday
4
+
5
+ # Extra pages compiled onto the end of an invoice.
6
+ #
7
+ # appendix = Payday::Appendix.new(
8
+ # source: File.read('appendix.typ'),
9
+ # inputs: {'shipments' => shipments.to_json},
10
+ # dependencies: {'chart.svg' => File.binread('chart.svg')}
11
+ # )
12
+ # invoice.render_pdf(appendix: appendix)
13
+ #
14
+ # +source+ is Typst markup appended to Payday's own template, so it is compiled as part of
15
+ # the invoice rather than as a second document that would then have to be merged in. Two
16
+ # things follow from that. The template's +#set page+ rules stay in force, so the appended
17
+ # pages keep the invoice's margins; and the page counter runs across the whole document, so
18
+ # the numbering footer counts the appended pages. A one-page invoice with one appended page
19
+ # numbers "1 / 2", and a three-page invoice continues into "4 / 4".
20
+ #
21
+ # That is also why the source is concatenated rather than +#include+d: an included file is
22
+ # its own module, and the page rules set inside the template would stop at its edge.
23
+ #
24
+ # **Pass your data through +inputs+, never through +source+.** Inputs reach the template as
25
+ # sys_inputs JSON, where Typst treats every string as literal text, which is what stops a
26
+ # customer-supplied field from affecting the layout. Source is code. Interpolating invoice
27
+ # data into it gives that data back the run of the document, and hands anyone who can set a
28
+ # field on an invoice a way to write Typst.
29
+ #
30
+ # +dependencies+ are files the source can reach by name -- +image("chart.svg")+ and the
31
+ # like. Keys are file names, values are the bytes.
32
+ Appendix = Data.define(:source, :inputs, :dependencies) do
33
+
34
+ # Accepts an Appendix, a Hash of the same attributes, or nil.
35
+ def self.wrap(value)
36
+ return value if value.nil? || value.is_a?(Appendix)
37
+
38
+ new(**value)
39
+ end
40
+
41
+ def initialize(source:, inputs: {}, dependencies: {})
42
+ super(source: source, inputs: inputs.transform_keys(&:to_s), dependencies: dependencies.transform_keys(&:to_s))
43
+ end
44
+
45
+ end
46
+
47
+ end
data/lib/payday/config.rb CHANGED
@@ -8,9 +8,9 @@ module Payday
8
8
 
9
9
  attr_accessor :invoice_logo, :company_name, :company_details, :date_format, :currency
10
10
 
11
- # Sets the page size to use. See the
12
- # {http://prawn.majesticseacreature.com/docs/0.10.2/Prawn/Document/PageGeometry.html Prawn documentation} for valid
13
- # page_size values.
11
+ # Sets the page size to use. Accepts the names Payday has always taken, 'LETTER', 'A4'
12
+ # and 'LEGAL', which the template maps onto the equivalent Typst paper. Anything else
13
+ # falls back to A4.
14
14
  attr_accessor :page_size
15
15
 
16
16
  # Returns the default configuration instance
@@ -23,7 +23,7 @@ module Payday
23
23
  # Primarily intended for use in our tests.
24
24
  def reset
25
25
  # TODO: Move into specs and make minimal configuration required (company name / details)
26
- self.invoice_logo = File.join(File.dirname(__FILE__), '..', '..', 'spec', 'assets', 'default_logo.png')
26
+ self.invoice_logo = File.expand_path('assets/default_logo.png', __dir__)
27
27
  self.company_name = 'Awesome Corp'
28
28
  self.company_details = 'awesomecorp@commondream.net'
29
29
  self.date_format = '%B %e, %Y'
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Payday
4
+
5
+ # Turns an Invoiceable into the plain Hash the Typst template consumes.
6
+ #
7
+ # Every currency, date and translation decision happens here, so the template stays a pure
8
+ # layout concern and receives nothing but literal strings.
9
+ class InvoicePresenter
10
+
11
+ def initialize(invoice)
12
+ @invoice = invoice
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ page_size: setting(:page_size),
18
+ company_name: setting(:company_name).strip,
19
+ company_details: company_details,
20
+ stamp: stamp,
21
+ bill_to: @invoice.bill_to,
22
+ ship_to: ship_to,
23
+ labels: labels,
24
+ details: details,
25
+ line_items: line_items,
26
+ totals: totals,
27
+ notes: Markup.to_runs(@invoice.notes),
28
+ qr_code: qr_code
29
+ }
30
+ end
31
+
32
+ def line_items
33
+ @invoice.line_items.map { |line| line_item(line) }
34
+ end
35
+
36
+ def totals
37
+ [[t('invoice.subtotal', 'Subtotal:'), money(@invoice.subtotal), false],
38
+ [tax_label, money(@invoice.tax), false],
39
+ *shipping_row,
40
+ *retention_row,
41
+ [t('invoice.total', 'Total:'), money(@invoice.total), true]]
42
+ end
43
+
44
+ def stamp
45
+ return t('status.refunded', 'REFUNDED') if @invoice.refunded?
46
+ return t('status.paid', 'PAID') if @invoice.paid?
47
+
48
+ t('status.overdue', 'OVERDUE') if @invoice.overdue?
49
+ end
50
+
51
+ def details
52
+ rows = [*number_row, *due_row, *paid_row]
53
+ @invoice.each_detail { |key, value| rows << [key.to_s, value.to_s] }
54
+ rows
55
+ end
56
+
57
+ def labels
58
+ {
59
+ bill_to: t('invoice.bill_to', 'Bill To'),
60
+ ship_to: t('invoice.ship_to', 'Ship To'),
61
+ notes: t('invoice.notes', 'Notes'),
62
+ description: t('line_item.description', 'Description'),
63
+ unit_price: t('line_item.unit_price', 'Unit Price'),
64
+ quantity: t('line_item.quantity', 'Quantity'),
65
+ amount: t('line_item.amount', 'Amount')
66
+ }
67
+ end
68
+
69
+ private
70
+
71
+ def shipping_row
72
+ [[shipping_label, money(@invoice.shipping), false]] if @invoice.shipping_rate.positive?
73
+ end
74
+
75
+ def retention_row
76
+ [[retention_label, money(-@invoice.retention), false]] if @invoice.retention_rate.positive?
77
+ end
78
+
79
+ def number_row
80
+ [[number_label, @invoice.invoice_number.to_s]] if @invoice.invoice_number
81
+ end
82
+
83
+ def due_row
84
+ [[t('invoice.due_date', 'Due Date:'), date(@invoice.due_at)]] if @invoice.due_at
85
+ end
86
+
87
+ def paid_row
88
+ [[t('invoice.paid_date', 'Paid Date:'), date(@invoice.paid_at)]] if @invoice.paid_at
89
+ end
90
+
91
+ def qr_code
92
+ return nil unless @invoice.respond_to?(:qr_code) && @invoice.qr_code.to_s.strip.present?
93
+
94
+ @invoice.qr_code.to_s
95
+ end
96
+
97
+ def line_item(line)
98
+ return predefined_line_item(line) if line.predefined_amount
99
+
100
+ {description: Markup.to_runs(line.description),
101
+ price: line.display_price || money(line.price),
102
+ quantity: line.display_quantity || BigDecimal(line.quantity.to_s).to_s('F'),
103
+ amount: money(line.amount)}
104
+ end
105
+
106
+ def predefined_line_item(line)
107
+ {description: Markup.to_runs(line.description), price: '', quantity: '',
108
+ amount: money(line.predefined_amount)}
109
+ end
110
+
111
+ def tax_label
112
+ @invoice.tax_description || t('invoice.tax', 'Tax:')
113
+ end
114
+
115
+ def shipping_label
116
+ @invoice.shipping_description || t('invoice.shipping', 'Shipping:')
117
+ end
118
+
119
+ def retention_label
120
+ @invoice.retention_description || t('invoice.retention', 'Retention:')
121
+ end
122
+
123
+ def money(number)
124
+ PdfRenderer.number_to_currency(number, @invoice)
125
+ end
126
+
127
+ # Prawn rendered each line of company_details as its own table cell, which collapsed any
128
+ # leading indentation. Config is commonly set from an indented heredoc, so strip it here
129
+ # rather than let it show up in the PDF.
130
+ def company_details
131
+ setting(:company_details).to_s.lines.map(&:strip).join("\n").strip
132
+ end
133
+
134
+ def ship_to
135
+ @invoice.ship_to if @invoice.respond_to?(:ship_to)
136
+ end
137
+
138
+ def number_label
139
+ return t('invoice.receipt_no', 'Receipt #:') if @invoice.paid?
140
+
141
+ t('invoice.invoice_no', 'Invoice #:')
142
+ end
143
+
144
+ def date(value)
145
+ return value.to_s unless value.is_a?(Date) || value.is_a?(Time)
146
+
147
+ value.strftime(Payday::Config.default.date_format)
148
+ end
149
+
150
+ def t(key, default)
151
+ I18n.t("payday.#{key}", default: default)
152
+ end
153
+
154
+ def setting(property)
155
+ return @invoice.send(property) if @invoice.respond_to?(property) && @invoice.send(property)
156
+
157
+ Payday::Config.default.send(property)
158
+ end
159
+
160
+ end
161
+
162
+ end
@@ -78,14 +78,15 @@ module Payday
78
78
  defined?(paid_at) && !!paid_at
79
79
  end
80
80
 
81
- # Renders this invoice to pdf as a string
82
- def render_pdf
83
- Payday::PdfRenderer.render(self)
81
+ # Renders this invoice to pdf as a string. Pass a Payday::Appendix to compile pages of
82
+ # your own onto the end of it.
83
+ def render_pdf(appendix: nil)
84
+ Payday::PdfRenderer.render(self, appendix: appendix)
84
85
  end
85
86
 
86
87
  # Renders this invoice to pdf
87
- def render_pdf_to_file(path)
88
- Payday::PdfRenderer.render_to_file(self, path)
88
+ def render_pdf_to_file(path, appendix: nil)
89
+ Payday::PdfRenderer.render_to_file(self, path, appendix: appendix)
89
90
  end
90
91
 
91
92
  # Iterates through the details on this invoiceable. The block given should accept
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Payday
4
+
5
+ # Converts the Prawn +inline_format+ markup Payday used to accept in notes and line item
6
+ # descriptions into structured runs that the Typst template can style.
7
+ #
8
+ # We deliberately produce data rather than Typst source. The template renders each run's
9
+ # +text+ as a literal string, so a customer-supplied description can never become Typst
10
+ # code. Never change this to emit markup.
11
+ class Markup
12
+
13
+ # Tags that simply switch a style on, mapped to the key they set on a run.
14
+ FLAGS = {'b' => :bold, 'i' => :italic, 'u' => :underline, 'strikethrough' => :strike,
15
+ 'sub' => :sub, 'sup' => :sup}.freeze
16
+
17
+ TAG = %r{<(?<name>b|i|u|strikethrough|sub|sup|font|color|link)(?<attrs>[^>]*)>(?<body>.*?)</\k<name>>}m
18
+ LINE_BREAK = %r{<br\s*/?>}
19
+
20
+ # Returns an Array of Hashes, each with a :text key and any of :bold, :italic, :underline,
21
+ # :strike, :sub, :sup, :size, :color and :link. Returns nil when there is nothing to render.
22
+ def self.to_runs(text)
23
+ return nil if text.nil?
24
+
25
+ runs(text.to_s.gsub(LINE_BREAK, "\n"), {})
26
+ end
27
+
28
+ def self.runs(text, style)
29
+ result = []
30
+ position = 0
31
+
32
+ while (match = TAG.match(text, position))
33
+ append(result, style, text[position...match.begin(0)])
34
+ result.concat(runs(match[:body], style.merge(style_for(match[:name], match[:attrs]))))
35
+ position = match.end(0)
36
+ end
37
+
38
+ append(result, style, text[position..])
39
+ result
40
+ end
41
+ private_class_method :runs
42
+
43
+ def self.append(result, style, text)
44
+ result << style.merge(text: text) unless text.nil? || text.empty?
45
+ end
46
+ private_class_method :append
47
+
48
+ def self.style_for(name, attrs)
49
+ return {FLAGS[name] => true} if FLAGS.key?(name)
50
+
51
+ case name
52
+ when 'font' then {size: attrs[/size=['"](\d+)['"]/, 1].to_i}
53
+ when 'color' then {color: attrs[/rgb=['"]#?(\h{6})['"]/, 1]}
54
+ when 'link' then {link: attrs[/href=['"](.*?)['"]/, 1]}
55
+ end
56
+ end
57
+ private_class_method :style_for
58
+
59
+ end
60
+
61
+ end