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 +4 -4
- data/lib/active_support_ext/object/deep_dup.rb +58 -0
- data/lib/active_support_ext/object/duplicable.rb +152 -0
- data/lib/grover.rb +42 -21
- data/lib/grover/middleware.rb +28 -1
- data/lib/grover/utils.rb +43 -6
- data/lib/grover/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ec8a1908f15407ef3d24ab28a2e211bee743b06
|
4
|
+
data.tar.gz: 96e00c18f6ebb1835e768807a6d9275aec88a95f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 =
|
69
|
-
|
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
|
-
|
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
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
data/lib/grover/middleware.rb
CHANGED
@@ -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)
|
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
|
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
|
-
#
|
48
|
+
# Deep transform the keys in an object (Hash/Array)
|
48
49
|
#
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
#
|
data/lib/grover/version.rb
CHANGED
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
|
+
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-
|
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
|