pdfkit 0.8.2 → 0.8.7.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pdfkit might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.github/workflows/release-drafter.yml +19 -0
- data/.github/workflows/stale.yml +19 -0
- data/.github/workflows/test.yml +59 -0
- data/.ruby-version +1 -1
- data/.travis.yml +42 -8
- data/CHANGELOG.md +58 -0
- data/Gemfile +1 -1
- data/README.md +36 -10
- data/lib/pdfkit/configuration.rb +38 -3
- data/lib/pdfkit/html_preprocessor.rb +25 -0
- data/lib/pdfkit/middleware.rb +53 -43
- data/lib/pdfkit/os.rb +21 -0
- data/lib/pdfkit/pdfkit.rb +50 -105
- data/lib/pdfkit/source.rb +8 -3
- data/lib/pdfkit/version.rb +3 -1
- data/lib/pdfkit/wkhtmltopdf.rb +82 -0
- data/lib/pdfkit.rb +6 -0
- data/pdfkit.gemspec +9 -7
- data/spec/configuration_spec.rb +85 -12
- data/spec/html_preprocessor_spec.rb +71 -0
- data/spec/middleware_spec.rb +237 -104
- data/spec/os_spec.rb +67 -0
- data/spec/pdfkit_spec.rb +47 -6
- data/spec/source_spec.rb +32 -0
- data/spec/spec_helper.rb +5 -1
- metadata +45 -48
data/lib/pdfkit/pdfkit.rb
CHANGED
@@ -1,40 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'shellwords'
|
2
|
-
require '
|
4
|
+
require 'tempfile'
|
3
5
|
|
4
6
|
class PDFKit
|
5
|
-
class
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class NoExecutableError < Error
|
6
10
|
def initialize
|
7
|
-
msg
|
8
|
-
|
11
|
+
msg = "No wkhtmltopdf executable found at #{PDFKit.configuration.wkhtmltopdf}\n" \
|
12
|
+
">> Please install wkhtmltopdf - https://github.com/pdfkit/PDFKit/wiki/Installing-WKHTMLTOPDF"
|
9
13
|
super(msg)
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
13
|
-
class ImproperSourceError <
|
17
|
+
class ImproperSourceError < Error
|
14
18
|
def initialize(msg)
|
15
19
|
super("Improper Source: #{msg}")
|
16
20
|
end
|
17
21
|
end
|
18
22
|
|
23
|
+
class ImproperWkhtmltopdfExitStatus < Error
|
24
|
+
def initialize(invoke)
|
25
|
+
super("Command failed (exitstatus=#{$?.exitstatus}): #{invoke}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
19
29
|
attr_accessor :source, :stylesheets
|
20
|
-
attr_reader :
|
30
|
+
attr_reader :renderer
|
21
31
|
|
22
32
|
def initialize(url_file_or_html, options = {})
|
23
33
|
@source = Source.new(url_file_or_html)
|
24
34
|
|
25
35
|
@stylesheets = []
|
26
36
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@
|
37
|
+
options = PDFKit.configuration.default_options.merge(options)
|
38
|
+
options.delete(:quiet) if PDFKit.configuration.verbose?
|
39
|
+
options.merge! find_options_in_meta(url_file_or_html) unless source.url?
|
40
|
+
@root_url = options.delete(:root_url)
|
41
|
+
@protocol = options.delete(:protocol)
|
42
|
+
@renderer = WkHTMLtoPDF.new options
|
43
|
+
@renderer.normalize_options
|
31
44
|
|
32
|
-
raise NoExecutableError
|
45
|
+
raise NoExecutableError unless File.exist?(PDFKit.configuration.wkhtmltopdf)
|
33
46
|
end
|
34
47
|
|
35
48
|
def command(path = nil)
|
36
|
-
args = @
|
37
|
-
shell_escaped_command = [executable, shell_escape_for_os(args)].join ' '
|
49
|
+
args = @renderer.options_for_command
|
50
|
+
shell_escaped_command = [executable, OS::shell_escape_for_os(args)].join ' '
|
38
51
|
|
39
52
|
# In order to allow for URL parameters (e.g. https://www.google.com/search?q=pdfkit) we do
|
40
53
|
# not escape the source. The user is responsible for ensuring that no vulnerabilities exist
|
@@ -45,17 +58,17 @@ class PDFKit
|
|
45
58
|
"#{shell_escaped_command} #{input_for_command} #{output_for_command}"
|
46
59
|
end
|
47
60
|
|
61
|
+
def options
|
62
|
+
# TODO(cdwort,sigmavirus24): Replace this with an attr_reader for @renderer instead in 1.0.0
|
63
|
+
@renderer.options
|
64
|
+
end
|
65
|
+
|
48
66
|
def executable
|
49
|
-
|
50
|
-
return default if default !~ /^\// # its not a path, so nothing we can do
|
51
|
-
if File.exist?(default)
|
52
|
-
default
|
53
|
-
else
|
54
|
-
default.split('/').last
|
55
|
-
end
|
67
|
+
PDFKit.configuration.executable
|
56
68
|
end
|
57
69
|
|
58
70
|
def to_pdf(path=nil)
|
71
|
+
preprocess_html
|
59
72
|
append_stylesheets
|
60
73
|
|
61
74
|
invoke = command(path)
|
@@ -68,7 +81,7 @@ class PDFKit
|
|
68
81
|
|
69
82
|
# $? is thread safe per
|
70
83
|
# http://stackoverflow.com/questions/2164887/thread-safe-external-process-in-ruby-plus-checking-exitstatus
|
71
|
-
raise
|
84
|
+
raise ImproperWkhtmltopdfExitStatus, invoke if empty_result?(path, result) || !successful?($?)
|
72
85
|
return result
|
73
86
|
end
|
74
87
|
|
@@ -79,14 +92,9 @@ class PDFKit
|
|
79
92
|
|
80
93
|
protected
|
81
94
|
|
82
|
-
# Pulled from:
|
83
|
-
# https://github.com/wkhtmltopdf/wkhtmltopdf/blob/ebf9b6cfc4c58a31349fb94c568b254fac37b3d3/README_WKHTMLTOIMAGE#L27
|
84
|
-
REPEATABLE_OPTIONS = %w[--allow --cookie --custom-header --post --post-file --run-script]
|
85
|
-
SPECIAL_OPTIONS = %w[cover toc]
|
86
|
-
|
87
95
|
def find_options_in_meta(content)
|
88
96
|
# Read file if content is a File
|
89
|
-
content = content.read if content.is_a?(File)
|
97
|
+
content = content.read if content.is_a?(File) || content.is_a?(Tempfile)
|
90
98
|
|
91
99
|
found = {}
|
92
100
|
content.scan(/<meta [^>]*>/) do |meta|
|
@@ -108,80 +116,38 @@ class PDFKit
|
|
108
116
|
end
|
109
117
|
|
110
118
|
def style_tag_for(stylesheet)
|
111
|
-
"<style>#{File.read(stylesheet)}</style>"
|
119
|
+
style = "<style>#{File.read(stylesheet)}</style>"
|
120
|
+
style = style.html_safe if style.respond_to?(:html_safe)
|
121
|
+
style
|
122
|
+
end
|
123
|
+
|
124
|
+
def preprocess_html
|
125
|
+
if @source.html?
|
126
|
+
processed_html = PDFKit::HTMLPreprocessor.process(@source.to_s, @root_url, @protocol)
|
127
|
+
@source = Source.new(processed_html)
|
128
|
+
end
|
112
129
|
end
|
113
130
|
|
114
131
|
def append_stylesheets
|
115
|
-
raise ImproperSourceError
|
132
|
+
raise ImproperSourceError, 'Stylesheets may only be added to an HTML source' if stylesheets.any? && !@source.html?
|
116
133
|
|
117
134
|
stylesheets.each do |stylesheet|
|
118
135
|
if @source.to_s.match(/<\/head>/)
|
119
|
-
@source = Source.new(@source.to_s.gsub(/(<\/head>)/) {|s|
|
136
|
+
@source = Source.new(@source.to_s.gsub(/(<\/head>)/) {|s|
|
137
|
+
style_tag_for(stylesheet) + (s.respond_to?(:html_safe) ? s.html_safe : s)
|
138
|
+
})
|
120
139
|
else
|
121
140
|
@source.to_s.insert(0, style_tag_for(stylesheet))
|
122
141
|
end
|
123
142
|
end
|
124
143
|
end
|
125
144
|
|
126
|
-
def normalize_options(options)
|
127
|
-
normalized_options = {}
|
128
|
-
|
129
|
-
options.each do |key, value|
|
130
|
-
next if !value
|
131
|
-
|
132
|
-
# The actual option for wkhtmltopdf
|
133
|
-
normalized_key = normalize_arg key
|
134
|
-
normalized_key = "--#{normalized_key}" unless SPECIAL_OPTIONS.include?(normalized_key)
|
135
|
-
|
136
|
-
# If the option is repeatable, attempt to normalize all values
|
137
|
-
if REPEATABLE_OPTIONS.include? normalized_key
|
138
|
-
normalize_repeatable_value(normalized_key, value) do |normalized_unique_key, normalized_value|
|
139
|
-
normalized_options[normalized_unique_key] = normalized_value
|
140
|
-
end
|
141
|
-
else # Otherwise, just normalize it like usual
|
142
|
-
normalized_options[normalized_key] = normalize_value(value)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
normalized_options
|
147
|
-
end
|
148
|
-
|
149
|
-
def normalize_arg(arg)
|
150
|
-
arg.to_s.downcase.gsub(/[^a-z0-9]/,'-')
|
151
|
-
end
|
152
|
-
|
153
|
-
def normalize_value(value)
|
154
|
-
case value
|
155
|
-
when nil
|
156
|
-
nil
|
157
|
-
when TrueClass, 'true' #ie, ==true, see http://www.ruby-doc.org/core-1.9.3/TrueClass.html
|
158
|
-
nil
|
159
|
-
when Hash
|
160
|
-
value.to_a.flatten.collect{|x| normalize_value(x)}.compact
|
161
|
-
when Array
|
162
|
-
value.flatten.collect{|x| x.to_s}
|
163
|
-
else
|
164
|
-
(host_is_windows? && value.to_s.index(' ')) ? "'#{ value.to_s }'" : value.to_s
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
def normalize_repeatable_value(option_name, value)
|
169
|
-
case value
|
170
|
-
when Hash, Array
|
171
|
-
value.each do |(key, val)|
|
172
|
-
yield [[option_name, normalize_value(key)], normalize_value(val)]
|
173
|
-
end
|
174
|
-
else
|
175
|
-
yield [[option_name, normalize_value(value)], nil]
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
145
|
def successful?(status)
|
180
146
|
return true if status.success?
|
181
147
|
|
182
148
|
# Some of the codes: https://code.google.com/p/wkhtmltopdf/issues/detail?id=1088
|
183
149
|
# returned when assets are missing (404): https://code.google.com/p/wkhtmltopdf/issues/detail?id=548
|
184
|
-
return true if status.exitstatus == 2 && error_handling?
|
150
|
+
return true if status.exitstatus == 2 && @renderer.error_handling?
|
185
151
|
|
186
152
|
false
|
187
153
|
end
|
@@ -189,25 +155,4 @@ class PDFKit
|
|
189
155
|
def empty_result?(path, result)
|
190
156
|
(path && File.size(path) == 0) || (path.nil? && result.to_s.strip.empty?)
|
191
157
|
end
|
192
|
-
|
193
|
-
def error_handling?
|
194
|
-
@options.key?('--ignore-load-errors') ||
|
195
|
-
# wkhtmltopdf v0.10.0 beta4 replaces ignore-load-errors with load-error-handling
|
196
|
-
# https://code.google.com/p/wkhtmltopdf/issues/detail?id=55
|
197
|
-
%w(skip ignore).include?(@options['--load-error-handling'])
|
198
|
-
end
|
199
|
-
|
200
|
-
def host_is_windows?
|
201
|
-
@host_is_windows ||= !(RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince/).nil?
|
202
|
-
end
|
203
|
-
|
204
|
-
def shell_escape_for_os(args)
|
205
|
-
if (host_is_windows?)
|
206
|
-
# Windows reserved shell characters are: & | ( ) < > ^
|
207
|
-
# See http://technet.microsoft.com/en-us/library/cc723564.aspx#XSLTsection123121120120
|
208
|
-
args.map { |arg| arg.gsub(/([&|()<>^])/,'^\1') }.join(" ")
|
209
|
-
else
|
210
|
-
args.shelljoin
|
211
|
-
end
|
212
|
-
end
|
213
158
|
end
|
data/lib/pdfkit/source.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
1
4
|
require 'uri'
|
2
5
|
|
3
6
|
class PDFKit
|
@@ -6,6 +9,8 @@ class PDFKit
|
|
6
9
|
|
7
10
|
def initialize(url_file_or_html)
|
8
11
|
@source = url_file_or_html
|
12
|
+
# @source is assumed to be modifiable, so make sure it is.
|
13
|
+
@source = @source.dup if @source.is_a?(String) && @source.frozen?
|
9
14
|
end
|
10
15
|
|
11
16
|
def url?
|
@@ -13,7 +18,7 @@ class PDFKit
|
|
13
18
|
end
|
14
19
|
|
15
20
|
def file?
|
16
|
-
@is_file ||= @source.kind_of?(File)
|
21
|
+
@is_file ||= @source.kind_of?(File) || @source.kind_of?(Tempfile)
|
17
22
|
end
|
18
23
|
|
19
24
|
def html?
|
@@ -37,11 +42,11 @@ class PDFKit
|
|
37
42
|
private
|
38
43
|
|
39
44
|
def shell_safe_url
|
40
|
-
url_needs_escaping? ? URI::escape(@source) : @source
|
45
|
+
url_needs_escaping? ? URI::DEFAULT_PARSER.escape(@source) : @source
|
41
46
|
end
|
42
47
|
|
43
48
|
def url_needs_escaping?
|
44
|
-
URI::
|
49
|
+
URI::DEFAULT_PARSER.escape(URI::DEFAULT_PARSER.unescape(@source)) != @source
|
45
50
|
end
|
46
51
|
end
|
47
52
|
end
|
data/lib/pdfkit/version.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class PDFKit
|
4
|
+
class WkHTMLtoPDF
|
5
|
+
attr_reader :options
|
6
|
+
# Pulled from:
|
7
|
+
# https://github.com/wkhtmltopdf/wkhtmltopdf/blob/ebf9b6cfc4c58a31349fb94c568b254fac37b3d3/README_WKHTMLTOIMAGE#L27
|
8
|
+
REPEATABLE_OPTIONS = %w[--allow --cookie --custom-header --post --post-file --run-script].freeze
|
9
|
+
SPECIAL_OPTIONS = %w[cover toc].freeze
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def normalize_options
|
16
|
+
# TODO(cdwort,sigmavirus24): Make this method idempotent in a future release so it can be called repeatedly
|
17
|
+
normalized_options = {}
|
18
|
+
|
19
|
+
@options.each do |key, value|
|
20
|
+
next if !value
|
21
|
+
|
22
|
+
# The actual option for wkhtmltopdf
|
23
|
+
normalized_key = normalize_arg key
|
24
|
+
normalized_key = "--#{normalized_key}" unless SPECIAL_OPTIONS.include?(normalized_key)
|
25
|
+
|
26
|
+
# If the option is repeatable, attempt to normalize all values
|
27
|
+
if REPEATABLE_OPTIONS.include? normalized_key
|
28
|
+
normalize_repeatable_value(normalized_key, value) do |normalized_unique_key, normalized_value|
|
29
|
+
normalized_options[normalized_unique_key] = normalized_value
|
30
|
+
end
|
31
|
+
else # Otherwise, just normalize it like usual
|
32
|
+
normalized_options[normalized_key] = normalize_value(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
@options = normalized_options
|
37
|
+
end
|
38
|
+
|
39
|
+
def error_handling?
|
40
|
+
@options.key?('--ignore-load-errors') ||
|
41
|
+
# wkhtmltopdf v0.10.0 beta4 replaces ignore-load-errors with load-error-handling
|
42
|
+
# https://code.google.com/p/wkhtmltopdf/issues/detail?id=55
|
43
|
+
%w(skip ignore).include?(@options['--load-error-handling'])
|
44
|
+
end
|
45
|
+
|
46
|
+
def options_for_command
|
47
|
+
@options.to_a.flatten.compact
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def normalize_arg(arg)
|
53
|
+
arg.to_s.downcase.gsub(/[^a-z0-9]/,'-')
|
54
|
+
end
|
55
|
+
|
56
|
+
def normalize_value(value)
|
57
|
+
case value
|
58
|
+
when nil
|
59
|
+
nil
|
60
|
+
when TrueClass, 'true' #ie, ==true, see http://www.ruby-doc.org/core-1.9.3/TrueClass.html
|
61
|
+
nil
|
62
|
+
when Hash
|
63
|
+
value.to_a.flatten.collect{|x| normalize_value(x)}.compact
|
64
|
+
when Array
|
65
|
+
value.flatten.collect{|x| x.to_s}
|
66
|
+
else
|
67
|
+
(OS::host_is_windows? && value.to_s.index(' ')) ? "'#{ value.to_s }'" : value.to_s
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def normalize_repeatable_value(option_name, value)
|
72
|
+
case value
|
73
|
+
when Hash, Array
|
74
|
+
value.each do |(key, val)|
|
75
|
+
yield [[option_name, normalize_value(key)], normalize_value(val)]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
yield [[option_name, normalize_value(value)], nil]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/pdfkit.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pdfkit/source'
|
2
4
|
require 'pdfkit/pdfkit'
|
3
5
|
require 'pdfkit/middleware'
|
6
|
+
require 'pdfkit/html_preprocessor'
|
7
|
+
require 'pdfkit/os'
|
4
8
|
require 'pdfkit/configuration'
|
9
|
+
require 'pdfkit/wkhtmltopdf'
|
10
|
+
require 'pdfkit/version'
|
data/pdfkit.gemspec
CHANGED
@@ -11,20 +11,22 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = "https://github.com/pdfkit/pdfkit"
|
12
12
|
s.summary = "HTML+CSS -> PDF"
|
13
13
|
s.description = "Uses wkhtmltopdf to create PDFs using HTML"
|
14
|
-
|
15
|
-
s.rubyforge_project = "pdfkit"
|
14
|
+
s.license = "MIT"
|
16
15
|
|
17
16
|
s.files = `git ls-files`.split("\n")
|
18
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
19
|
s.require_paths = ["lib"]
|
21
20
|
|
22
|
-
|
23
|
-
|
21
|
+
s.required_ruby_version = ">= 2.5"
|
22
|
+
|
23
|
+
s.requirements << "wkhtmltopdf"
|
24
|
+
|
25
|
+
# Development Dependencies
|
26
|
+
s.add_development_dependency(%q<activesupport>, [">= 4.1.11"])
|
24
27
|
s.add_development_dependency(%q<mocha>, [">= 0.9.10"])
|
25
28
|
s.add_development_dependency(%q<rack-test>, [">= 0.5.6"])
|
26
|
-
s.add_development_dependency(%q<
|
27
|
-
s.add_development_dependency(%q<
|
28
|
-
s.add_development_dependency(%q<rdoc>, ["~> 4.0.1"])
|
29
|
+
s.add_development_dependency(%q<rake>, [">= 12.3.3"])
|
30
|
+
s.add_development_dependency(%q<rdoc>, [">= 4.0.1"])
|
29
31
|
s.add_development_dependency(%q<rspec>, ["~> 3.0"])
|
30
32
|
end
|
data/spec/configuration_spec.rb
CHANGED
@@ -1,28 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe PDFKit::Configuration do
|
4
6
|
subject { PDFKit::Configuration.new }
|
5
7
|
describe "#wkhtmltopdf" do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
context "when explicitly configured" do
|
9
|
+
it "uses configured value and don't detect" do
|
10
|
+
expect(subject).not_to receive(:default_wkhtmltopdf)
|
11
|
+
subject.wkhtmltopdf = "./Gemfile" # Need a file which exists
|
12
|
+
expect(subject.wkhtmltopdf).to eq("./Gemfile")
|
13
|
+
end
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
it "falls back to detected binary if configured path doesn't exists" do
|
16
|
+
expect(subject).to receive(:default_wkhtmltopdf).twice.and_return("/bin/fallback")
|
17
|
+
expect(subject).to receive(:warn).with(/No executable found/)
|
18
|
+
subject.wkhtmltopdf = "./missing-file" # Need a file which doesn't exist
|
19
|
+
expect(subject.wkhtmltopdf).to eq("/bin/fallback")
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
context "when not explicitly configured" do
|
18
|
-
|
19
|
-
#
|
20
|
-
|
21
|
-
|
24
|
+
context "when running inside bundler" do
|
25
|
+
# Simulate the presence of bundler even if it's not here
|
26
|
+
before { stub_const("Bundler::GemfileError", Class) }
|
27
|
+
|
28
|
+
it "detects the existance of bundler" do
|
29
|
+
expect(subject).to receive(:`).with('bundle exec which wkhtmltopdf').and_return("c:\\windows\\path.exe\n")
|
30
|
+
expect(subject.wkhtmltopdf).to eq('c:\windows\path.exe')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "falls back if bundler path fails" do
|
34
|
+
# This happens when there is a wrong (buggy) version of bundler for example
|
35
|
+
expect(subject).to receive(:`).with('bundle exec which wkhtmltopdf').and_return("")
|
36
|
+
expect(subject).to receive(:`).with('which wkhtmltopdf').and_return("c:\\windows\\path.exe\n")
|
37
|
+
expect(subject.wkhtmltopdf).to eq('c:\windows\path.exe')
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns last line of 'bundle exec which' output" do
|
41
|
+
# Happens when the user does not have a HOME directory on their system and runs bundler < 2
|
42
|
+
expect(subject).to receive(:`).with('bundle exec which wkhtmltopdf').and_return(<<~EOT
|
43
|
+
`/home/myuser` is not a directory.
|
44
|
+
Bundler will use `/tmp/bundler/home/myuser' as your home directory temporarily.
|
45
|
+
/usr/bin/wkhtmltopdf
|
46
|
+
EOT
|
47
|
+
)
|
48
|
+
expect(subject.wkhtmltopdf).to eq('/usr/bin/wkhtmltopdf')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when running without bundler" do
|
53
|
+
# Simulate the absence of bundler even if it's there
|
54
|
+
before { hide_const("Bundler::GemfileError") }
|
55
|
+
|
56
|
+
it "detects the existance of bundler" do
|
57
|
+
expect(subject).not_to receive(:`).with('bundle exec which wkhtmltopdf')
|
58
|
+
expect(subject).to receive(:`).with('which wkhtmltopdf').and_return('c:\windows\path.exe')
|
59
|
+
expect(subject.wkhtmltopdf).to eq('c:\windows\path.exe')
|
60
|
+
end
|
22
61
|
end
|
23
62
|
end
|
24
63
|
end
|
25
64
|
|
65
|
+
describe "#executable" do
|
66
|
+
it "returns wkhtmltopdf by default" do
|
67
|
+
expect(subject.executable).to eql subject.wkhtmltopdf
|
68
|
+
end
|
69
|
+
|
70
|
+
it "uses xvfb-run wrapper when option of using xvfb is configured" do
|
71
|
+
expect(subject).to receive(:using_xvfb?).and_return(true)
|
72
|
+
expect(subject.executable).to include 'xvfb-run'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
26
76
|
describe "#default_options" do
|
27
77
|
it "sets defaults for the command options" do
|
28
78
|
expect(subject.default_options[:disable_smart_shrinking]).to eql false
|
@@ -40,6 +90,13 @@ describe PDFKit::Configuration do
|
|
40
90
|
expect(subject.default_options[:quiet]).to eql false
|
41
91
|
expect(subject.default_options[:is_awesome]).to eql true
|
42
92
|
end
|
93
|
+
|
94
|
+
it "merges additional options with existing defaults" do
|
95
|
+
subject.default_options = { quiet: false, is_awesome: true }
|
96
|
+
expect(subject.default_options[:quiet]).to eql false
|
97
|
+
expect(subject.default_options[:is_awesome]).to eql true
|
98
|
+
expect(subject.default_options[:disable_smart_shrinking]).to eql false
|
99
|
+
end
|
43
100
|
end
|
44
101
|
|
45
102
|
describe "#root_url" do
|
@@ -64,6 +121,22 @@ describe PDFKit::Configuration do
|
|
64
121
|
end
|
65
122
|
end
|
66
123
|
|
124
|
+
describe "#using_xvfb?" do
|
125
|
+
it "can be configured to true" do
|
126
|
+
subject.use_xvfb = true
|
127
|
+
expect(subject.using_xvfb?).to eql true
|
128
|
+
end
|
129
|
+
|
130
|
+
it "defaults to false" do
|
131
|
+
expect(subject.using_xvfb?).to eql false
|
132
|
+
end
|
133
|
+
|
134
|
+
it "can be configured to false" do
|
135
|
+
subject.use_xvfb = false
|
136
|
+
expect(subject.using_xvfb?).to eql false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
67
140
|
describe "#verbose?" do
|
68
141
|
it "can be configured to true" do
|
69
142
|
subject.verbose = true
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe PDFKit::HTMLPreprocessor do
|
6
|
+
describe "#process" do
|
7
|
+
let(:preprocessor) { PDFKit::HTMLPreprocessor }
|
8
|
+
let(:root_url) { 'http://example.com/' } # This mirrors Middleware#root_url's response
|
9
|
+
let(:protocol) { 'http' }
|
10
|
+
|
11
|
+
it "correctly parses host-relative url with single quotes" do
|
12
|
+
original_body = %{<html><head><link href='/stylesheets/application.css' media='screen' rel='stylesheet' type='text/css' /></head><body><img alt='test' src="/test.png" /></body></html>}
|
13
|
+
body = preprocessor.process original_body, root_url, protocol
|
14
|
+
expect(body).to eq("<html><head><link href='http://example.com/stylesheets/application.css' media='screen' rel='stylesheet' type='text/css' /></head><body><img alt='test' src=\"http://example.com/test.png\" /></body></html>")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "correctly parses host-relative url with double quotes" do
|
18
|
+
original_body = %{<link href="/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />}
|
19
|
+
body = preprocessor.process original_body, root_url, protocol
|
20
|
+
expect(body).to eq("<link href=\"http://example.com/stylesheets/application.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "correctly parses protocol-relative url with single quotes" do
|
24
|
+
original_body = %{<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'>}
|
25
|
+
body = preprocessor.process original_body, root_url, protocol
|
26
|
+
expect(body).to eq("<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'>")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "correctly parses protocol-relative url with double quotes" do
|
30
|
+
original_body = %{<link href="//fonts.googleapis.com/css?family=Open+Sans:400,600" rel='stylesheet' type='text/css'>}
|
31
|
+
body = preprocessor.process original_body, root_url, protocol
|
32
|
+
expect(body).to eq("<link href=\"http://fonts.googleapis.com/css?family=Open+Sans:400,600\" rel='stylesheet' type='text/css'>")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "correctly parses multiple tags where first one is root url" do
|
36
|
+
original_body = %{<a href='/'><img src='/logo.jpg' ></a>}
|
37
|
+
body = preprocessor.process original_body, root_url, protocol
|
38
|
+
expect(body).to eq "<a href='http://example.com/'><img src='http://example.com/logo.jpg' ></a>"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "returns the body even if there are no valid substitutions found" do
|
42
|
+
original_body = "NO MATCH"
|
43
|
+
body = preprocessor.process original_body, root_url, protocol
|
44
|
+
expect(body).to eq("NO MATCH")
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when root_url is nil' do
|
48
|
+
it "returns the body safely, without interpolating" do
|
49
|
+
original_body = %{<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'><a href='/'><img src='/logo.jpg'></a>}
|
50
|
+
body = preprocessor.process original_body, nil, protocol
|
51
|
+
expect(body).to eq(%{<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'><a href='/'><img src='/logo.jpg'></a>})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when protocol is nil' do
|
56
|
+
it "returns the body safely, without interpolating" do
|
57
|
+
original_body = %{<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'><a href='/'><img src='/logo.jpg'></a>}
|
58
|
+
body = preprocessor.process original_body, root_url, nil
|
59
|
+
expect(body).to eq(%{<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'><a href='http://example.com/'><img src='http://example.com/logo.jpg'></a>})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when root_url and protocol are both nil' do
|
64
|
+
it "returns the body safely, without interpolating" do
|
65
|
+
original_body = %{<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'><a href='/'><img src='/logo.jpg'></a>}
|
66
|
+
body = preprocessor.process original_body, nil, nil
|
67
|
+
expect(body).to eq original_body
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|