thousand_island 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|