grover 0.4.4 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|