grover 0.4.4 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 21fdefff2c21b5341c4d824e0fd2b1a497f23636
4
- data.tar.gz: 423949075eecb0abd2ba0ac9fa77493fa7131861
3
+ metadata.gz: 2ec8a1908f15407ef3d24ab28a2e211bee743b06
4
+ data.tar.gz: 96e00c18f6ebb1835e768807a6d9275aec88a95f
5
5
  SHA512:
6
- metadata.gz: a5a62083e5deabebf8728033b0519d1ffadc7cd08683a5d1ad85e25fe490d83113d41db34db24bde1f4b89c0c709e0b46e424ee02ca23d96ff82f8fe79911e6d
7
- data.tar.gz: 6fad9c43307155729a8425d1553953ae1ce22e75a2a12f7f6d6aa7c755e18339130205b36e9dbe3f379ac0a0e0b3bb1ff1c2e8b63ead6867669b9c64332cd789
6
+ metadata.gz: 62002773ddebdff4bcf9dcc0bc6a4fde4d68c5922e1ce14c5fb07704f815462fbf04b737a34402de2a240feb79d3eff816eb8d0e2e60ebe04f8b0e0a81259a21
7
+ data.tar.gz: 7016d579dcba337bf2beb558883ca0fec597e9d1b06f7dedecfd86fe63c6572be82a34cc9926a8dace39ab150f725f0a43c7f37f17ad4ab2bec2c3ccabc9bfee
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copied from active support
4
+ # @see active_support/core_ext/object/duplicable.rb
5
+
6
+ require 'active_support_ext/object/duplicable'
7
+
8
+ class Object
9
+ # Returns a deep copy of object if it's duplicable. If it's
10
+ # not duplicable, returns +self+.
11
+ #
12
+ # object = Object.new
13
+ # dup = object.deep_dup
14
+ # dup.instance_variable_set(:@a, 1)
15
+ #
16
+ # object.instance_variable_defined?(:@a) # => false
17
+ # dup.instance_variable_defined?(:@a) # => true
18
+ def deep_dup
19
+ duplicable? ? dup : self
20
+ end
21
+ end
22
+
23
+ class Array
24
+ # Returns a deep copy of array.
25
+ #
26
+ # array = [1, [2, 3]]
27
+ # dup = array.deep_dup
28
+ # dup[1][2] = 4
29
+ #
30
+ # array[1][2] # => nil
31
+ # dup[1][2] # => 4
32
+ def deep_dup
33
+ map(&:deep_dup)
34
+ end
35
+ end
36
+
37
+ class Hash
38
+ # Returns a deep copy of hash.
39
+ #
40
+ # hash = { a: { b: 'b' } }
41
+ # dup = hash.deep_dup
42
+ # dup[:a][:c] = 'c'
43
+ #
44
+ # hash[:a][:c] # => nil
45
+ # dup[:a][:c] # => "c"
46
+ def deep_dup
47
+ hash = dup
48
+ each_pair do |key, value|
49
+ if key.frozen? && key.is_a?(::String)
50
+ hash[key] = value.deep_dup
51
+ else
52
+ hash.delete(key)
53
+ hash[key.deep_dup] = value.deep_dup
54
+ end
55
+ end
56
+ hash
57
+ end
58
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copied from active support
4
+ # @see active_support/core_ext/object/duplicable.rb
5
+
6
+ #--
7
+ # Most objects are cloneable, but not all. For example you can't dup methods:
8
+ #
9
+ # method(:puts).dup # => TypeError: allocator undefined for Method
10
+ #
11
+ # Classes may signal their instances are not duplicable removing +dup+/+clone+
12
+ # or raising exceptions from them. So, to dup an arbitrary object you normally
13
+ # use an optimistic approach and are ready to catch an exception, say:
14
+ #
15
+ # arbitrary_object.dup rescue object
16
+ #
17
+ # Rails dups objects in a few critical spots where they are not that arbitrary.
18
+ # That rescue is very expensive (like 40 times slower than a predicate), and it
19
+ # is often triggered.
20
+ #
21
+ # That's why we hardcode the following cases and check duplicable? instead of
22
+ # using that rescue idiom.
23
+ #++
24
+ class Object
25
+ # Can you safely dup this object?
26
+ #
27
+ # False for method objects;
28
+ # true otherwise.
29
+ def duplicable?
30
+ true
31
+ end
32
+ end
33
+
34
+ class NilClass
35
+ begin
36
+ nil.dup
37
+ rescue TypeError
38
+ # +nil+ is not duplicable:
39
+ #
40
+ # nil.duplicable? # => false
41
+ # nil.dup # => TypeError: can't dup NilClass
42
+ def duplicable?
43
+ false
44
+ end
45
+ end
46
+ end
47
+
48
+ class FalseClass
49
+ begin
50
+ false.dup
51
+ rescue TypeError
52
+ # +false+ is not duplicable:
53
+ #
54
+ # false.duplicable? # => false
55
+ # false.dup # => TypeError: can't dup FalseClass
56
+ def duplicable?
57
+ false
58
+ end
59
+ end
60
+ end
61
+
62
+ class TrueClass
63
+ begin
64
+ true.dup
65
+ rescue TypeError
66
+ # +true+ is not duplicable:
67
+ #
68
+ # true.duplicable? # => false
69
+ # true.dup # => TypeError: can't dup TrueClass
70
+ def duplicable?
71
+ false
72
+ end
73
+ end
74
+ end
75
+
76
+ class Symbol
77
+ begin
78
+ :symbol.dup # Ruby 2.4.x.
79
+ 'symbol_from_string'.to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
80
+ rescue TypeError
81
+ # Symbols are not duplicable:
82
+ #
83
+ # :my_symbol.duplicable? # => false
84
+ # :my_symbol.dup # => TypeError: can't dup Symbol
85
+ def duplicable?
86
+ false
87
+ end
88
+ end
89
+ end
90
+
91
+ class Numeric
92
+ begin
93
+ 1.dup
94
+ rescue TypeError
95
+ # Numbers are not duplicable:
96
+ #
97
+ # 3.duplicable? # => false
98
+ # 3.dup # => TypeError: can't dup Integer
99
+ def duplicable?
100
+ false
101
+ end
102
+ end
103
+ end
104
+
105
+ require 'bigdecimal'
106
+ class BigDecimal
107
+ # BigDecimals are duplicable:
108
+ #
109
+ # BigDecimal("1.2").duplicable? # => true
110
+ # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
111
+ def duplicable?
112
+ true
113
+ end
114
+ end
115
+
116
+ class Method
117
+ # Methods are not duplicable:
118
+ #
119
+ # method(:puts).duplicable? # => false
120
+ # method(:puts).dup # => TypeError: allocator undefined for Method
121
+ def duplicable?
122
+ false
123
+ end
124
+ end
125
+
126
+ class Complex
127
+ begin
128
+ Complex(1).dup
129
+ rescue TypeError
130
+ # Complexes are not duplicable:
131
+ #
132
+ # Complex(1).duplicable? # => false
133
+ # Complex(1).dup # => TypeError: can't copy Complex
134
+ def duplicable?
135
+ false
136
+ end
137
+ end
138
+ end
139
+
140
+ class Rational
141
+ begin
142
+ Rational(1).dup
143
+ rescue TypeError
144
+ # Rationals are not duplicable:
145
+ #
146
+ # Rational(1).duplicable? # => false
147
+ # Rational(1).dup # => TypeError: can't copy Rational
148
+ def duplicable?
149
+ false
150
+ end
151
+ end
152
+ end
data/lib/grover.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  require 'grover/version'
2
2
 
3
3
  require 'grover/utils'
4
+ require 'active_support_ext/object/deep_dup'
5
+
4
6
  require 'grover/html_preprocessor'
5
7
  require 'grover/middleware'
6
8
  require 'grover/configuration'
7
9
 
8
- require 'schmooze'
9
10
  require 'nokogiri'
11
+ require 'schmooze'
10
12
 
11
13
  #
12
14
  # Grover interface for converting HTML to PDF
@@ -58,6 +60,8 @@ class Grover
58
60
  <div class='text right'><span class='pageNumber'></span>/<span class='totalPages'></span></div>
59
61
  HTML
60
62
 
63
+ attr_reader :front_cover_path, :back_cover_path
64
+
61
65
  #
62
66
  # @param [String] url URL of the page to convert
63
67
  # @param [Hash] options Optional parameters to pass to PDF processor
@@ -65,8 +69,11 @@ class Grover
65
69
  #
66
70
  def initialize(url, options = {})
67
71
  @url = url
68
- @options = Grover.configuration.options.merge options
69
- @root_path = @options.delete :root_path
72
+ @options = combine_options options
73
+
74
+ @root_path = @options.delete 'root_path'
75
+ @front_cover_path = @options.delete 'front_cover_path'
76
+ @back_cover_path = @options.delete 'back_cover_path'
70
77
  end
71
78
 
72
79
  #
@@ -76,10 +83,30 @@ class Grover
76
83
  # @return [String] The resulting PDF data
77
84
  #
78
85
  def to_pdf(path = nil)
79
- result = processor.convert_pdf @url, normalized_options(path)
86
+ normalized_options = Utils.normalize_object @options
87
+ normalized_options['path'] = path if path.is_a? ::String
88
+ result = processor.convert_pdf @url, normalized_options
80
89
  result['data'].pack('c*')
81
90
  end
82
91
 
92
+ #
93
+ # Returns whether a front cover (request) path has been specified in the options
94
+ #
95
+ # @return [Boolean] Front cover path is configured
96
+ #
97
+ def show_front_cover?
98
+ front_cover_path.is_a?(::String) && front_cover_path.start_with?('/')
99
+ end
100
+
101
+ #
102
+ # Returns whether a back cover (request) path has been specified in the options
103
+ #
104
+ # @return [Boolean] Back cover path is configured
105
+ #
106
+ def show_back_cover?
107
+ back_cover_path.is_a?(::String) && back_cover_path.start_with?('/')
108
+ end
109
+
83
110
  #
84
111
  # Instance inspection
85
112
  #
@@ -113,11 +140,16 @@ class Grover
113
140
  Processor.new(root_path)
114
141
  end
115
142
 
116
- def base_options
117
- options = {}
118
- @options.each { |k, v| options[k.to_s] = v }
119
- options.merge! meta_options unless url_source?
120
- options
143
+ def combine_options(options)
144
+ combined = Utils.deep_stringify_keys Grover.configuration.options
145
+ Utils.deep_merge! combined, Utils.deep_stringify_keys(options)
146
+ Utils.deep_merge! combined, meta_options unless url_source?
147
+
148
+ fix_templates! combined
149
+ fix_boolean_options! combined
150
+ fix_numeric_options! combined
151
+
152
+ combined
121
153
  end
122
154
 
123
155
  #
@@ -144,17 +176,6 @@ class Grover
144
176
  @url.match(/^http/i)
145
177
  end
146
178
 
147
- def normalized_options(path)
148
- options = base_options
149
-
150
- fix_templates! options
151
- fix_boolean_options! options
152
- fix_numeric_options! options
153
- options['path'] = path if path
154
-
155
- Utils.normalize_object options
156
- end
157
-
158
179
  def fix_templates!(options)
159
180
  display_url = options.delete 'display_url'
160
181
  return unless display_url
@@ -162,7 +183,7 @@ class Grover
162
183
  options['footer_template'] ||= DEFAULT_FOOTER_TEMPLATE
163
184
 
164
185
  %w[header_template footer_template].each do |key|
165
- next unless options[key].is_a? String
186
+ next unless options[key].is_a? ::String
166
187
 
167
188
  options[key] = options[key].gsub(DISPLAY_URL_PLACEHOLDER, display_url)
168
189
  end
@@ -1,3 +1,5 @@
1
+ require 'combine_pdf'
2
+
1
3
  class Grover
2
4
  #
3
5
  # Rack middleware for catching PDF requests and returning the upstream HTML as a PDF
@@ -44,11 +46,36 @@ class Grover
44
46
  end
45
47
 
46
48
  def convert_to_pdf(response)
49
+ grover = create_grover_for_response(response)
50
+ if grover.show_front_cover? || grover.show_back_cover?
51
+ add_cover_content grover
52
+ else
53
+ grover.to_pdf
54
+ end
55
+ end
56
+
57
+ def create_grover_for_response(response)
47
58
  body = response.respond_to?(:body) ? response.body : response.join
48
59
  body = body.join if body.is_a?(Array)
49
60
 
50
61
  body = HTMLPreprocessor.process body, root_url, protocol
51
- Grover.new(body, display_url: request_url).to_pdf
62
+ Grover.new(body, display_url: request_url)
63
+ end
64
+
65
+ def add_cover_content(grover)
66
+ pdf = CombinePDF.parse grover.to_pdf
67
+ pdf >> fetch_cover_pdf(grover.front_cover_path) if grover.show_front_cover?
68
+ pdf << fetch_cover_pdf(grover.back_cover_path) if grover.show_back_cover?
69
+ pdf.to_pdf
70
+ end
71
+
72
+ def fetch_cover_pdf(path)
73
+ temp_env = env.deep_dup
74
+ temp_env['PATH_INFO'], temp_env['QUERY_STRING'] = path.split '?'
75
+ _, _, response = @app.call(temp_env)
76
+ response.close if response.respond_to? :close
77
+ grover = create_grover_for_response response
78
+ CombinePDF.parse grover.to_pdf
52
79
  end
53
80
 
54
81
  def update_headers(headers, body)
data/lib/grover/utils.rb CHANGED
@@ -23,7 +23,8 @@ class Grover
23
23
  #
24
24
  # Remove minimum spaces from the front of all lines within a string
25
25
  #
26
- # Based on active_support/core_ext/string/strip.rb
26
+ # Based on active support
27
+ # @see active_support/core_ext/string/strip.rb
27
28
  #
28
29
  def self.strip_heredoc(string, inline: false)
29
30
  string = string.gsub(/^#{string.scan(/^[ \t]*(?=\S)/).min}/, ''.freeze)
@@ -44,18 +45,54 @@ class Grover
44
45
  end
45
46
 
46
47
  #
47
- # Recursively normalizes hash objects with camelized string keys
48
+ # Deep transform the keys in an object (Hash/Array)
48
49
  #
49
- def self.normalize_object(object)
50
- if object.is_a? Hash
51
- object.each_with_object({}) do |(k, v), acc|
52
- acc[normalize_key(k)] = normalize_object(v)
50
+ # Copied from active support
51
+ # @see active_support/core_ext/hash/keys.rb
52
+ #
53
+ def self.deep_transform_keys_in_object(object, &block)
54
+ case object
55
+ when Hash
56
+ object.each_with_object({}) do |(key, value), result|
57
+ result[yield(key)] = deep_transform_keys_in_object(value, &block)
53
58
  end
59
+ when Array
60
+ object.map { |e| deep_transform_keys_in_object(e, &block) }
54
61
  else
55
62
  object
56
63
  end
57
64
  end
58
65
 
66
+ #
67
+ # Deep transform the keys in the hash to strings
68
+ #
69
+ def self.deep_stringify_keys(hash)
70
+ deep_transform_keys_in_object hash, &:to_s
71
+ end
72
+
73
+ #
74
+ # Deep merge a hash with another hash
75
+ #
76
+ # Based on active support
77
+ # @see active_support/core_ext/hash/deep_merge.rb
78
+ #
79
+ def self.deep_merge!(hash, other_hash)
80
+ hash.merge!(other_hash) do |_, this_val, other_val|
81
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
82
+ deep_merge! this_val.dup, other_val
83
+ else
84
+ other_val
85
+ end
86
+ end
87
+ end
88
+
89
+ #
90
+ # Recursively normalizes hash objects with camelized string keys
91
+ #
92
+ def self.normalize_object(object)
93
+ deep_transform_keys_in_object(object) { |k| normalize_key(k) }
94
+ end
95
+
59
96
  #
60
97
  # Normalizes hash keys into camelized strings, including up-casing known acronyms
61
98
  #
@@ -1,3 +1,3 @@
1
1
  class Grover
2
- VERSION = '0.4.4'.freeze
2
+ VERSION = '0.5.1'.freeze
3
3
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grover
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Bromwich
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-10 00:00:00.000000000 Z
11
+ date: 2018-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: combine_pdf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: nokogiri
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -157,6 +171,8 @@ executables: []
157
171
  extensions: []
158
172
  extra_rdoc_files: []
159
173
  files:
174
+ - lib/active_support_ext/object/deep_dup.rb
175
+ - lib/active_support_ext/object/duplicable.rb
160
176
  - lib/grover.rb
161
177
  - lib/grover/configuration.rb
162
178
  - lib/grover/html_preprocessor.rb