thousand_island 0.0.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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/Guardfile +23 -0
- data/LICENSE.txt +22 -0
- data/README.md +363 -0
- data/Rakefile +1 -0
- data/lib/thousand_island.rb +21 -0
- data/lib/thousand_island/builder.rb +153 -0
- data/lib/thousand_island/components.rb +4 -0
- data/lib/thousand_island/components/base.rb +43 -0
- data/lib/thousand_island/components/body.rb +18 -0
- data/lib/thousand_island/components/footer.rb +85 -0
- data/lib/thousand_island/components/header.rb +24 -0
- data/lib/thousand_island/style_sheet.rb +112 -0
- data/lib/thousand_island/template.rb +280 -0
- data/lib/thousand_island/utilities/style_hash.rb +20 -0
- data/lib/thousand_island/utilities/utilities.rb +58 -0
- data/lib/thousand_island/version.rb +3 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/thousand_island/builder_spec.rb +106 -0
- data/spec/thousand_island/components/base_spec.rb +68 -0
- data/spec/thousand_island/components/body_spec.rb +37 -0
- data/spec/thousand_island/components/footer_spec.rb +57 -0
- data/spec/thousand_island/components/header_spec.rb +40 -0
- data/spec/thousand_island/style_sheet_spec.rb +44 -0
- data/spec/thousand_island/template_spec.rb +150 -0
- data/spec/thousand_island/utilities/style_hash_spec.rb +29 -0
- data/spec/thousand_island/utilities/utilities_spec.rb +82 -0
- data/spec/thousand_island_spec.rb +3 -0
- data/thousand_island.gemspec +30 -0
- metadata +200 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
module ThousandIsland
|
2
|
+
# The Template class is where you can define elements that may be common to
|
3
|
+
# all (or some) documents within your application. It is likely that a common
|
4
|
+
# style will be required, so defining it in a Template and then using that
|
5
|
+
# Template subclass in any custom Builders DRYs up your pdf generation, as
|
6
|
+
# well as allowing for easy restyling across the whole application.
|
7
|
+
#
|
8
|
+
# Typically, the Template subclass would define the settings for the PrawnDocument,
|
9
|
+
# as well as the settings for the header and footer. See the Docs below for the
|
10
|
+
# <code>settings</code> method for the defaults. Add your own or override any
|
11
|
+
# existing settings in the <code>settings</code> method. Any options passed into
|
12
|
+
# the constructor as a Hash will be merged with these settings, and the defaults.
|
13
|
+
#
|
14
|
+
# Content for the header and footer will be defined in the methods
|
15
|
+
# <code>header_content</code> and <code>footer_content</code>. These methods are
|
16
|
+
# passed as a block when the pdf is rendered. Any standard Prawn methods may be
|
17
|
+
# used (including bounding boxes or any other layout tools). In addition, any
|
18
|
+
# of the styles from the <code>StyleSheet</code> can be applied as helper methods.
|
19
|
+
# For instance, the default style sheet has a <code>h1_style</code> method that
|
20
|
+
# returns a ThousandIsland::StyleHash, so in your code you can use:
|
21
|
+
# h1 "My Document Header"
|
22
|
+
# and Prawn will render the text in the style set in the <code>h1_style</code>
|
23
|
+
# ThousandIsland::StyleHash.
|
24
|
+
#
|
25
|
+
# In addition to the supplied style methods, you can create a custom method:
|
26
|
+
# def magic_style
|
27
|
+
# ThousandIsland::StyleHash.new({
|
28
|
+
# size: 15
|
29
|
+
# style: bold
|
30
|
+
# })
|
31
|
+
# end
|
32
|
+
# As long as the method ends in the word "_style" and returns a Hash, you magically
|
33
|
+
# get to do this:
|
34
|
+
# magic "My magic text is bold and size 15!!"
|
35
|
+
# The method may return a standard Hash, but it is safer to return a
|
36
|
+
# ThousandIsland::StyleHash, as this dynamically duplicates a few keys to accommodate
|
37
|
+
# using the style in normal Prawn text methods as well as formatted text boxes, which
|
38
|
+
# use a slightly different convention. You don't have to worry about that if you use
|
39
|
+
# the ThousandIsland::StyleHash.
|
40
|
+
#
|
41
|
+
# Alternatively, your method could do this:
|
42
|
+
# def magic_style
|
43
|
+
# h1_style.merge({
|
44
|
+
# size: 15
|
45
|
+
# style: bold
|
46
|
+
# })
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# The following is an example of a custom template that subclasses
|
50
|
+
# ThousandIsland::Template -
|
51
|
+
#
|
52
|
+
# class MyTemplate < ThousandIsland::Template
|
53
|
+
# include MyCustomStyleSheet # optional
|
54
|
+
#
|
55
|
+
# # settings here are merged with and override the defaults
|
56
|
+
# def settings
|
57
|
+
# {
|
58
|
+
# header: {
|
59
|
+
# height: 55,
|
60
|
+
# render:true,
|
61
|
+
# repeated: true
|
62
|
+
# },
|
63
|
+
# footer: {
|
64
|
+
# render:true,
|
65
|
+
# height: 9,
|
66
|
+
# numbering_string: 'Page <page> of <total>',
|
67
|
+
# repeated: true
|
68
|
+
# }
|
69
|
+
# }
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# def header_content
|
73
|
+
# pdf.image "#{pdf_images_path}/company_logo.png", height: 30 # Standard Prawn syntax
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# def footer_content
|
77
|
+
# footer "www.mycompanyurl.com" # Using the magic method we get from the footer_style
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# def pdf_images_path
|
81
|
+
# "#{Rails.root}/app/assets/pdf_images" # This is entirely up to you
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# Nb.
|
86
|
+
# The Footer is a three column layout, with the numbering on the right column
|
87
|
+
# and the content defined here in the middle. More flexibility will be added
|
88
|
+
# in a later version.
|
89
|
+
#
|
90
|
+
# Optional:
|
91
|
+
#
|
92
|
+
# Add a <code>body_content</code> method to add content before whatever the
|
93
|
+
# Builder defines in it's method of the same name.
|
94
|
+
#
|
95
|
+
class Template
|
96
|
+
include ThousandIsland::StyleSheet
|
97
|
+
|
98
|
+
attr_reader :pdf, :pdf_options
|
99
|
+
|
100
|
+
def initialize(options={})
|
101
|
+
setup_document_options(options)
|
102
|
+
setup_prawn_document
|
103
|
+
calculate_bounds
|
104
|
+
end
|
105
|
+
|
106
|
+
# Override in inheriting class to override defaults. The default settings
|
107
|
+
# are:
|
108
|
+
# page_size: 'A4',
|
109
|
+
# page_layout: :portrait,
|
110
|
+
# left_margin: 54,
|
111
|
+
# right_margin: 54,
|
112
|
+
# header: {
|
113
|
+
# render: true,
|
114
|
+
# height: 33,
|
115
|
+
# bottom_padding: 20,
|
116
|
+
# repeated: true
|
117
|
+
# },
|
118
|
+
# footer: {
|
119
|
+
# render: true,
|
120
|
+
# height: 33,
|
121
|
+
# top_padding: 20,
|
122
|
+
# repeated: true,
|
123
|
+
# number_pages: true,
|
124
|
+
# numbering_string: '<page>',
|
125
|
+
# numbering_options: {
|
126
|
+
# align: :right,
|
127
|
+
# start_count_at: 1,
|
128
|
+
# }
|
129
|
+
# The settings in the hash will be merged with the default settings. Any Prawn
|
130
|
+
# setting <i>should</i> be valid at the top level of the hash.
|
131
|
+
# The styles used in the Header and Footer are determined by the default styles in the
|
132
|
+
# StyleSheet, but can be overridden in your Template class or by building your own StyleSheet
|
133
|
+
def settings
|
134
|
+
{}
|
135
|
+
end
|
136
|
+
|
137
|
+
def draw_body(&block)
|
138
|
+
body_obj.draw do
|
139
|
+
body_content if respond_to? :body_content
|
140
|
+
yield if block_given?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def draw_header
|
145
|
+
header_obj.draw do
|
146
|
+
header_content if respond_to? :header_content
|
147
|
+
yield if block_given?
|
148
|
+
end if render_header?
|
149
|
+
end
|
150
|
+
|
151
|
+
def draw_footer(&block)
|
152
|
+
footer_obj.draw do
|
153
|
+
yield if block_given?
|
154
|
+
footer_content &block if respond_to? :footer_content
|
155
|
+
end if render_footer?
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def render_header?
|
161
|
+
pdf_options[:header][:render]
|
162
|
+
end
|
163
|
+
|
164
|
+
def render_footer?
|
165
|
+
pdf_options[:footer][:render]
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
def calculate_bounds
|
171
|
+
pdf_options[:body][:top] = body_start
|
172
|
+
pdf_options[:body][:height] = body_height
|
173
|
+
end
|
174
|
+
|
175
|
+
def body_start
|
176
|
+
@body_start ||= pdf.bounds.height - header_space
|
177
|
+
end
|
178
|
+
|
179
|
+
def body_height
|
180
|
+
@body_height ||= body_start - footer_space
|
181
|
+
end
|
182
|
+
|
183
|
+
def header_space
|
184
|
+
return (pdf_options[:header][:height] + pdf_options[:header][:bottom_padding]) if pdf_options[:header][:render]
|
185
|
+
0
|
186
|
+
end
|
187
|
+
|
188
|
+
def footer_space
|
189
|
+
return (pdf_options[:footer][:height] + pdf_options[:footer][:top_padding]) if pdf_options[:footer][:render]
|
190
|
+
0
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
def setup_prawn_document
|
195
|
+
@pdf = Prawn::Document.new(pdf_options)
|
196
|
+
end
|
197
|
+
|
198
|
+
def setup_document_options(options={})
|
199
|
+
@pdf_options = deep_merger.merge_options(options, settings, defaults, component_defaults)
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
def component_defaults
|
204
|
+
components = {
|
205
|
+
footer: footer_klass.defaults,
|
206
|
+
header: header_klass.defaults,
|
207
|
+
body: body_klass.defaults,
|
208
|
+
}
|
209
|
+
components[:footer][:style] = footer_style
|
210
|
+
components
|
211
|
+
end
|
212
|
+
|
213
|
+
def header_klass
|
214
|
+
Components::Header
|
215
|
+
end
|
216
|
+
|
217
|
+
def header_obj
|
218
|
+
@header ||= header_klass.new(pdf, pdf_options[:header])
|
219
|
+
end
|
220
|
+
|
221
|
+
def footer_klass
|
222
|
+
Components::Footer
|
223
|
+
end
|
224
|
+
|
225
|
+
def footer_obj
|
226
|
+
@footer ||= footer_klass.new(pdf, pdf_options[:footer])
|
227
|
+
end
|
228
|
+
|
229
|
+
def body_klass
|
230
|
+
Components::Body
|
231
|
+
end
|
232
|
+
|
233
|
+
def body_obj
|
234
|
+
@body ||= body_klass.new(pdf, pdf_options[:body])
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
|
239
|
+
def deep_merger
|
240
|
+
@deep_merger ||= Utilities::DeepMerge
|
241
|
+
end
|
242
|
+
|
243
|
+
# Called by method missing when a style is supplied with text, ie: h1 'Header'
|
244
|
+
def render_with_style(style, output)
|
245
|
+
style_values = send("#{style}_style")
|
246
|
+
pdf.text output, style_values
|
247
|
+
end
|
248
|
+
|
249
|
+
def defaults
|
250
|
+
{
|
251
|
+
page_size: 'A4',
|
252
|
+
page_layout: :portrait,
|
253
|
+
left_margin: 54,
|
254
|
+
right_margin: 54,
|
255
|
+
header: {
|
256
|
+
render: true,
|
257
|
+
},
|
258
|
+
footer: {
|
259
|
+
render: true,
|
260
|
+
},
|
261
|
+
body: {},
|
262
|
+
}
|
263
|
+
end
|
264
|
+
|
265
|
+
#Respond to methods that relate to the style_sheet known styles
|
266
|
+
def method_missing(method_name, *arguments, &block)
|
267
|
+
style_method = "#{method_name}_style"
|
268
|
+
if respond_to?(style_method)
|
269
|
+
render_with_style(method_name, arguments[0])
|
270
|
+
else
|
271
|
+
super
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def respond_to_missing?(method_name, *)
|
276
|
+
available_styles.include?(method_name) || super
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ThousandIsland
|
2
|
+
# A subclass of Hash, automatically adds keys that mirror other keys to allow
|
3
|
+
# for a couple of small differences in the Prawn options hashes:
|
4
|
+
# :font_style = :style
|
5
|
+
# :styles = :style and puts it into an Array
|
6
|
+
class StyleHash < Hash
|
7
|
+
def initialize(style={})
|
8
|
+
super()
|
9
|
+
self.merge!(style)
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
return self[:size] if key == :font_size
|
14
|
+
return [self[:style]] if key == :styles
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ThousandIsland
|
2
|
+
module Utilities
|
3
|
+
|
4
|
+
module DeepMerge
|
5
|
+
|
6
|
+
#Take a number of hashes and merge them into one, respecting
|
7
|
+
# the structure and nesting according to the pdf options hash.
|
8
|
+
# Hashes work in order of precedence, the first in the array
|
9
|
+
# overrides, the second, etc.
|
10
|
+
#
|
11
|
+
# @param hashes [*Hash] A number of hashes to merge, in the order of precedence
|
12
|
+
#
|
13
|
+
# @return [Hash] the merged values
|
14
|
+
def self.merge_options(*hashes)
|
15
|
+
hashes.reverse!
|
16
|
+
merged = {}
|
17
|
+
footer = merge_footer(*hashes)
|
18
|
+
header = merge_header(*hashes)
|
19
|
+
body = merge_body(*hashes)
|
20
|
+
hashes.each do |h|
|
21
|
+
merged.merge!(h)
|
22
|
+
end
|
23
|
+
merged[:footer] = footer
|
24
|
+
merged[:header] = header
|
25
|
+
merged[:body] = body
|
26
|
+
merged
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.merge_footer(*hashes)
|
30
|
+
keys = [:numbering_options, :style]
|
31
|
+
merge_for_key_and_nested_keys(:footer, keys, *hashes)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.merge_header(*hashes)
|
35
|
+
merge_for_key_and_nested_keys(:header, [], *hashes)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.merge_body(*hashes)
|
39
|
+
merge_for_key_and_nested_keys(:body, [], *hashes)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.merge_for_key_and_nested_keys(key, keys, *hashes)
|
43
|
+
temp = {}
|
44
|
+
merged = {}
|
45
|
+
hashes.each do |h|
|
46
|
+
keys.each do |k|
|
47
|
+
temp[k] = {} unless temp.has_key? k
|
48
|
+
temp[k].merge!(h[key][k]) if h[key] && h[key][k]
|
49
|
+
end
|
50
|
+
merged.merge!(h[key]) if h[key]
|
51
|
+
end
|
52
|
+
merged.merge(temp)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
4
|
+
require 'thousand_island'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
if config.files_to_run.one?
|
8
|
+
config.default_formatter = 'doc'
|
9
|
+
end
|
10
|
+
config.order = :random
|
11
|
+
|
12
|
+
config.mock_with :rspec do |mocks|
|
13
|
+
mocks.verify_doubled_constant_names = true
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ThousandIsland
|
2
|
+
describe Builder do
|
3
|
+
|
4
|
+
context 'template' do
|
5
|
+
|
6
|
+
it 'raises with no template' do
|
7
|
+
klass = described_class.dup
|
8
|
+
builder = klass.new
|
9
|
+
expect{ builder.build }.to raise_error(TemplateRequiredError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'method instantiates a new instance of the template_class' do
|
13
|
+
klass = described_class.dup
|
14
|
+
template = double(:template)
|
15
|
+
allow(template).to receive(:available_styles) {[]}
|
16
|
+
expect(template).to receive(:new).with({}) { template }
|
17
|
+
expect(template).to receive(:pdf) {}
|
18
|
+
klass.uses_template(template)
|
19
|
+
klass.new.send(:template)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'with instance' do
|
24
|
+
|
25
|
+
describe 'content method is called when it' do
|
26
|
+
let(:template) { ThousandIsland::Template.new }
|
27
|
+
let(:klass) { described_class.dup }
|
28
|
+
let(:builder) { klass.new }
|
29
|
+
|
30
|
+
before :each do
|
31
|
+
allow(builder).to receive(:template) { template }
|
32
|
+
# klass.send(:define_method, :header_content, ->{})
|
33
|
+
# klass.send(:define_method, :body_content, ->{})
|
34
|
+
# klass.send(:define_method, :footer_content, ->{})
|
35
|
+
end
|
36
|
+
context 'exists' do
|
37
|
+
|
38
|
+
it 'header_content' do
|
39
|
+
# allow(builder).to receive(:header_content)
|
40
|
+
expect(builder).to receive(:header_content)
|
41
|
+
builder.send(:draw_header)
|
42
|
+
end
|
43
|
+
it 'body_content' do
|
44
|
+
expect(builder).to receive(:body_content)
|
45
|
+
builder.send(:draw_body)
|
46
|
+
end
|
47
|
+
it 'footer_content' do
|
48
|
+
expect(builder).to receive(:footer_content)
|
49
|
+
builder.send(:draw_footer)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
context 'does not exist' do
|
53
|
+
it 'header_content' do
|
54
|
+
expect(builder).to_not respond_to(:header_content)
|
55
|
+
end
|
56
|
+
it 'body_content' do
|
57
|
+
expect(builder).to_not respond_to(:body_content)
|
58
|
+
end
|
59
|
+
it 'footer_content' do
|
60
|
+
expect(builder).to_not respond_to(:footer_content)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'calls template method' do
|
66
|
+
let(:template) { ThousandIsland::Template.new }
|
67
|
+
let(:pdf) { instance_double('Prawn::Document') }
|
68
|
+
let(:klass) { described_class.dup }
|
69
|
+
let(:builder) { klass.new }
|
70
|
+
|
71
|
+
before :each do
|
72
|
+
allow(template).to receive(:draw_body) { template }
|
73
|
+
allow(template).to receive(:draw_header) { template }
|
74
|
+
allow(template).to receive(:draw_footer) { template }
|
75
|
+
allow(template).to receive(:available_styles) { [:h1] }
|
76
|
+
allow(pdf).to receive(:render)
|
77
|
+
allow(builder).to receive(:template) { template }
|
78
|
+
allow(builder).to receive(:pdf) { pdf }
|
79
|
+
end
|
80
|
+
|
81
|
+
it '#draw_body' do
|
82
|
+
expect(template).to receive(:draw_body)
|
83
|
+
builder.build
|
84
|
+
end
|
85
|
+
|
86
|
+
it '#header' do
|
87
|
+
expect(template).to receive(:draw_header)
|
88
|
+
builder.build
|
89
|
+
end
|
90
|
+
|
91
|
+
it '#draw_footer' do
|
92
|
+
expect(template).to receive(:draw_footer)
|
93
|
+
builder.build
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'style methods' do
|
97
|
+
it 'h1' do
|
98
|
+
allow(template).to receive(:h1)
|
99
|
+
expect(template).to receive(:available_styles)
|
100
|
+
builder.h1 'text'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|