weasyprint 0.1.0

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.
@@ -0,0 +1,23 @@
1
+ class WeasyPrint
2
+ class Source
3
+ def initialize(url_file_or_html)
4
+ @source = url_file_or_html
5
+ end
6
+
7
+ def url?
8
+ @source.is_a?(String) && @source.match(/\Ahttp/)
9
+ end
10
+
11
+ def file?
12
+ @source.kind_of?(File)
13
+ end
14
+
15
+ def html?
16
+ !(url? || file?)
17
+ end
18
+
19
+ def to_s
20
+ file? ? @source.path : @source
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ class WeasyPrint
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,154 @@
1
+ require 'shellwords'
2
+
3
+ class WeasyPrint
4
+
5
+ class NoExecutableError < StandardError
6
+ def initialize
7
+ msg = "No weasyprint executable found at #{WeasyPrint.configuration.weasyprint}\n"
8
+ msg << ">> Please install weasyprint - http://weasyprint.org/docs/install/"
9
+ super(msg)
10
+ end
11
+ end
12
+
13
+ class ImproperSourceError < StandardError
14
+ def initialize(msg)
15
+ super("Improper Source: #{msg}")
16
+ end
17
+ end
18
+
19
+ attr_accessor :source, :stylesheets
20
+ attr_reader :options
21
+
22
+ def initialize(url_file_or_html, options = {})
23
+ @source = Source.new(url_file_or_html)
24
+
25
+ @stylesheets = []
26
+
27
+ @options = WeasyPrint.configuration.default_options.merge(options)
28
+ @options = normalize_options(@options)
29
+
30
+ raise NoExecutableError.new unless File.exists?(WeasyPrint.configuration.weasyprint)
31
+ end
32
+
33
+ def command(path = nil)
34
+ args = [executable]
35
+ args += @options.to_a.flatten.compact
36
+
37
+ if @source.html?
38
+ args << '-' # Get HTML from stdin
39
+ else
40
+ args << @source.to_s
41
+ end
42
+
43
+ args << (path || '-') # Write to file or stdout
44
+
45
+ args.shelljoin
46
+ end
47
+
48
+ def executable
49
+ default = WeasyPrint.configuration.weasyprint
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
56
+ end
57
+
58
+ def to_pdf(path=nil)
59
+ append_stylesheets
60
+
61
+ invoke = command(path)
62
+
63
+ result = IO.popen(invoke, "wb+") do |pdf|
64
+ pdf.puts(@source.to_s) if @source.html?
65
+ pdf.close_write
66
+ pdf.gets(nil)
67
+ end
68
+ result = File.read(path) if path
69
+
70
+ # $? is thread safe per http://stackoverflow.com/questions/2164887/thread-safe-external-process-in-ruby-plus-checking-exitstatus
71
+ raise "command failed (exitstatus=#{$?.exitstatus}): #{invoke}" if result.to_s.strip.empty? or !successful?($?)
72
+ return result
73
+ end
74
+
75
+ def to_file(path)
76
+ self.to_pdf(path)
77
+ File.new(path)
78
+ end
79
+
80
+ protected
81
+
82
+ REPEATABLE_OPTIONS = %w[]
83
+
84
+ def style_tag_for(stylesheet)
85
+ "<style>#{File.read(stylesheet)}</style>"
86
+ end
87
+
88
+ def append_stylesheets
89
+ raise ImproperSourceError.new('Stylesheets may only be added to an HTML source') if stylesheets.any? && !@source.html?
90
+
91
+ stylesheets.each do |stylesheet|
92
+ if @source.to_s.match(/<\/head>/)
93
+ @source = Source.new(@source.to_s.gsub(/(<\/head>)/) {|s| style_tag_for(stylesheet) + s })
94
+ else
95
+ @source.to_s.insert(0, style_tag_for(stylesheet))
96
+ end
97
+ end
98
+ end
99
+
100
+ def normalize_options(options)
101
+ normalized_options = {}
102
+
103
+ options.each do |key, value|
104
+ next if !value
105
+
106
+ # The actual option for weasyprint
107
+ normalized_key = "--#{normalize_arg key}"
108
+
109
+ # If the option is repeatable, attempt to normalize all values
110
+ if REPEATABLE_OPTIONS.include? normalized_key
111
+ normalize_repeatable_value(value) do |normalized_key_piece, normalized_value|
112
+ normalized_options[[normalized_key, normalized_key_piece]] = normalized_value
113
+ end
114
+ else # Otherwise, just normalize it like usual
115
+ normalized_options[normalized_key] = normalize_value(value)
116
+ end
117
+ end
118
+
119
+ normalized_options
120
+ end
121
+
122
+ def normalize_arg(arg)
123
+ arg.to_s.downcase.gsub(/[^a-z0-9]/,'-')
124
+ end
125
+
126
+ def normalize_value(value)
127
+ case value
128
+ when TrueClass, 'true' #ie, ==true, see http://www.ruby-doc.org/core-1.9.3/TrueClass.html
129
+ nil
130
+ when Hash
131
+ value.to_a.flatten.collect{|x| normalize_value(x)}.compact
132
+ when Array
133
+ value.flatten.collect{|x| x.to_s}
134
+ else
135
+ value.to_s
136
+ end
137
+ end
138
+
139
+ def normalize_repeatable_value(value)
140
+ case value
141
+ when Hash, Array
142
+ value.each do |(key, value)|
143
+ yield [normalize_value(key), normalize_value(value)]
144
+ end
145
+ else
146
+ [normalize_value(value), '']
147
+ end
148
+ end
149
+
150
+ def successful?(status)
151
+ status.success?
152
+ end
153
+
154
+ end
@@ -0,0 +1 @@
1
+ body { font-size: 20px; }
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ <h1>Oh Hai!</h1>
4
+ </body>
5
+ </html>
@@ -0,0 +1,3 @@
1
+ .test:before {
2
+ content: '\2039';
3
+ }
@@ -0,0 +1,384 @@
1
+ require 'spec_helper'
2
+
3
+ def app; Rack::Lint.new(@app); end
4
+
5
+ def mock_app(options = {}, conditions = {}, custom_headers = {})
6
+ main_app = lambda { |env|
7
+ @env = env
8
+ full_headers = headers.merge custom_headers
9
+ [200, full_headers, @body || ['Hello world!']]
10
+ }
11
+
12
+ builder = Rack::Builder.new
13
+ builder.use WeasyPrint::Middleware, options, conditions
14
+ builder.run main_app
15
+ @app = builder.to_app
16
+ end
17
+
18
+ describe WeasyPrint::Middleware do
19
+ let(:headers) { {'Content-Type' => "text/html"} }
20
+
21
+ describe "#call" do
22
+ describe "caching" do
23
+ let(:headers) { {'Content-Type' => "text/html", 'ETag' => 'foo', 'Cache-Control' => 'max-age=2592000, public'} }
24
+
25
+ context "by default" do
26
+ before { mock_app }
27
+
28
+ it "deletes ETag" do
29
+ get 'http://www.example.org/public/test.pdf'
30
+ expect(last_response.headers["ETag"]).to be_nil
31
+ end
32
+ it "deletes Cache-Control" do
33
+ get 'http://www.example.org/public/test.pdf'
34
+ expect(last_response.headers["Cache-Control"]).to be_nil
35
+ end
36
+ end
37
+
38
+ context "when on" do
39
+ before { mock_app({}, :caching => true) }
40
+
41
+ it "preserves ETag" do
42
+ get 'http://www.example.org/public/test.pdf'
43
+ expect(last_response.headers["ETag"]).not_to be_nil
44
+ end
45
+ it "preserves Cache-Control" do
46
+ get 'http://www.example.org/public/test.pdf'
47
+ expect(last_response.headers["Cache-Control"]).not_to be_nil
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "conditions" do
53
+ describe ":only" do
54
+
55
+ describe "regex" do
56
+ describe "one" do
57
+ before { mock_app({}, :only => %r[^/public]) }
58
+
59
+ context "matching" do
60
+ specify do
61
+ get 'http://www.example.org/public/test.pdf'
62
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
63
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
64
+ end
65
+ end
66
+
67
+ context "not matching" do
68
+ specify do
69
+ get 'http://www.example.org/secret/test.pdf'
70
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
71
+ expect(last_response.body).to eq("Hello world!")
72
+ end
73
+ end
74
+ end # one regex
75
+
76
+ describe "multiple" do
77
+ before { mock_app({}, :only => [%r[^/invoice], %r[^/public]]) }
78
+
79
+ context "matching" do
80
+ specify do
81
+ get 'http://www.example.org/public/test.pdf'
82
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
83
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
84
+ end
85
+ end
86
+
87
+ context "not matching" do
88
+ specify do
89
+ get 'http://www.example.org/secret/test.pdf'
90
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
91
+ expect(last_response.body).to eq("Hello world!")
92
+ end
93
+ end
94
+ end # multiple regex
95
+ end # regex
96
+
97
+ describe "string" do
98
+ describe "one" do
99
+ before { mock_app({}, :only => '/public') }
100
+
101
+ context "matching" do
102
+ specify do
103
+ get 'http://www.example.org/public/test.pdf'
104
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
105
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
106
+ end
107
+ end
108
+
109
+ context "not matching" do
110
+ specify do
111
+ get 'http://www.example.org/secret/test.pdf'
112
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
113
+ expect(last_response.body).to eq("Hello world!")
114
+ end
115
+ end
116
+ end # one string
117
+
118
+ describe "multiple" do
119
+ before { mock_app({}, :only => ['/invoice', '/public']) }
120
+
121
+ context "matching" do
122
+ specify do
123
+ get 'http://www.example.org/public/test.pdf'
124
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
125
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
126
+ end
127
+ end
128
+
129
+ context "not matching" do
130
+ specify do
131
+ get 'http://www.example.org/secret/test.pdf'
132
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
133
+ expect(last_response.body).to eq("Hello world!")
134
+ end
135
+ end
136
+ end # multiple string
137
+ end # string
138
+
139
+ end
140
+
141
+ describe ":except" do
142
+
143
+ describe "regex" do
144
+ describe "one" do
145
+ before { mock_app({}, :except => %r[^/secret]) }
146
+
147
+ context "matching" do
148
+ specify do
149
+ get 'http://www.example.org/public/test.pdf'
150
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
151
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
152
+ end
153
+ end
154
+
155
+ context "not matching" do
156
+ specify do
157
+ get 'http://www.example.org/secret/test.pdf'
158
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
159
+ expect(last_response.body).to eq("Hello world!")
160
+ end
161
+ end
162
+ end # one regex
163
+
164
+ describe "multiple" do
165
+ before { mock_app({}, :except => [%r[^/prawn], %r[^/secret]]) }
166
+
167
+ context "matching" do
168
+ specify do
169
+ get 'http://www.example.org/public/test.pdf'
170
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
171
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
172
+ end
173
+ end
174
+
175
+ context "not matching" do
176
+ specify do
177
+ get 'http://www.example.org/secret/test.pdf'
178
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
179
+ expect(last_response.body).to eq("Hello world!")
180
+ end
181
+ end
182
+ end # multiple regex
183
+ end # regex
184
+
185
+ describe "string" do
186
+ describe "one" do
187
+ before { mock_app({}, :except => '/secret') }
188
+
189
+ context "matching" do
190
+ specify do
191
+ get 'http://www.example.org/public/test.pdf'
192
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
193
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
194
+ end
195
+ end
196
+
197
+ context "not matching" do
198
+ specify do
199
+ get 'http://www.example.org/secret/test.pdf'
200
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
201
+ expect(last_response.body).to eq("Hello world!")
202
+ end
203
+ end
204
+ end # one string
205
+
206
+ describe "multiple" do
207
+ before { mock_app({}, :except => ['/prawn', '/secret']) }
208
+
209
+ context "matching" do
210
+ specify do
211
+ get 'http://www.example.org/public/test.pdf'
212
+ expect(last_response.headers["Content-Type"]).to eq("application/pdf")
213
+ expect(last_response.body.bytesize).to eq(WeasyPrint.new("Hello world!").to_pdf.bytesize)
214
+ end
215
+ end
216
+
217
+ context "not matching" do
218
+ specify do
219
+ get 'http://www.example.org/secret/test.pdf'
220
+ expect(last_response.headers["Content-Type"]).to eq("text/html")
221
+ expect(last_response.body).to eq("Hello world!")
222
+ end
223
+ end
224
+ end # multiple string
225
+ end # string
226
+
227
+ end
228
+
229
+ describe "saving generated pdf to disk" do
230
+ before do
231
+ #make sure tests don't find an old test_save.pdf
232
+ File.delete('spec/test_save.pdf') if File.exists?('spec/test_save.pdf')
233
+ expect(File.exists?('spec/test_save.pdf')).to be_false
234
+ end
235
+
236
+ context "when header WeasyPrint-save-pdf is present" do
237
+ it "should saved the .pdf to disk" do
238
+ headers = { 'WeasyPrint-save-pdf' => 'spec/test_save.pdf' }
239
+ mock_app({}, {only: '/public'}, headers)
240
+ get 'http://www.example.org/public/test_save.pdf'
241
+ expect(File.exists?('spec/test_save.pdf')).to be_true
242
+ end
243
+
244
+ it "should not raise when target directory does not exist" do
245
+ headers = { 'WeasyPrint-save-pdf' => '/this/dir/does/not/exist/spec/test_save.pdf' }
246
+ mock_app({}, {only: '/public'}, headers)
247
+ expect {
248
+ get 'http://www.example.com/public/test_save.pdf'
249
+ }.not_to raise_error
250
+ end
251
+ end
252
+
253
+ context "when header WeasyPrint-save-pdf is not present" do
254
+ it "should not saved the .pdf to disk" do
255
+ mock_app({}, {only: '/public'}, {} )
256
+ get 'http://www.example.org/public/test_save.pdf'
257
+ expect(File.exists?('spec/test_save.pdf')).to be_false
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ describe "remove .pdf from PATH_INFO and REQUEST_URI" do
264
+ before { mock_app }
265
+
266
+ context "matching" do
267
+
268
+ specify do
269
+ get 'http://www.example.org/public/file.pdf'
270
+ expect(@env["PATH_INFO"]).to eq("/public/file")
271
+ expect(@env["REQUEST_URI"]).to eq("/public/file")
272
+ expect(@env["SCRIPT_NAME"]).to be_empty
273
+ end
274
+ specify do
275
+ get 'http://www.example.org/public/file.txt'
276
+ expect(@env["PATH_INFO"]).to eq("/public/file.txt")
277
+ expect(@env["REQUEST_URI"]).to be_nil
278
+ expect(@env["SCRIPT_NAME"]).to be_empty
279
+ end
280
+ end
281
+
282
+ context "subdomain matching" do
283
+ before do
284
+ main_app = lambda { |env|
285
+ @env = env
286
+ @env['SCRIPT_NAME'] = '/example.org'
287
+ headers = {'Content-Type' => "text/html"}
288
+ [200, headers, @body || ['Hello world!']]
289
+ }
290
+
291
+ builder = Rack::Builder.new
292
+ builder.use WeasyPrint::Middleware
293
+ builder.run main_app
294
+ @app = builder.to_app
295
+ end
296
+ specify do
297
+ get 'http://example.org/sub/public/file.pdf'
298
+ expect(@env["PATH_INFO"]).to eq("/sub/public/file")
299
+ expect(@env["REQUEST_URI"]).to eq("/sub/public/file")
300
+ expect(@env["SCRIPT_NAME"]).to eq("/example.org")
301
+ end
302
+ specify do
303
+ get 'http://example.org/sub/public/file.txt'
304
+ expect(@env["PATH_INFO"]).to eq("/sub/public/file.txt")
305
+ expect(@env["REQUEST_URI"]).to be_nil
306
+ expect(@env["SCRIPT_NAME"]).to eq("/example.org")
307
+ end
308
+ end
309
+
310
+ end
311
+ end
312
+
313
+ describe "#translate_paths" do
314
+ before do
315
+ @pdf = WeasyPrint::Middleware.new({})
316
+ @env = { 'REQUEST_URI' => 'http://example.com/document.pdf', 'rack.url_scheme' => 'http', 'HTTP_HOST' => 'example.com' }
317
+ end
318
+
319
+ it "should correctly parse relative url with single quotes" do
320
+ @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>}
321
+ body = @pdf.send :translate_paths, @body, @env
322
+ 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>")
323
+ end
324
+
325
+ it "should correctly parse relative url with double quotes" do
326
+ @body = %{<link href="/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />}
327
+ body = @pdf.send :translate_paths, @body, @env
328
+ expect(body).to eq("<link href=\"http://example.com/stylesheets/application.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />")
329
+ end
330
+
331
+ it "should correctly parse relative url with double quotes" do
332
+ @body = %{<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'>}
333
+ body = @pdf.send :translate_paths, @body, @env
334
+ expect(body).to eq("<link href='//fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'>")
335
+ end
336
+
337
+ it "should return the body even if there are no valid substitutions found" do
338
+ @body = "NO MATCH"
339
+ body = @pdf.send :translate_paths, @body, @env
340
+ expect(body).to eq("NO MATCH")
341
+ end
342
+ end
343
+
344
+ describe "#translate_paths with root_url configuration" do
345
+ before do
346
+ @pdf = WeasyPrint::Middleware.new({})
347
+ @env = { 'REQUEST_URI' => 'http://example.com/document.pdf', 'rack.url_scheme' => 'http', 'HTTP_HOST' => 'example.com' }
348
+ WeasyPrint.configure do |config|
349
+ config.root_url = "http://example.net/"
350
+ end
351
+ end
352
+
353
+ it "should add the root_url" do
354
+ @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>}
355
+ body = @pdf.send :translate_paths, @body, @env
356
+ expect(body).to eq("<html><head><link href='http://example.net/stylesheets/application.css' media='screen' rel='stylesheet' type='text/css' /></head><body><img alt='test' src=\"http://example.net/test.png\" /></body></html>")
357
+ end
358
+
359
+ after do
360
+ WeasyPrint.configure do |config|
361
+ config.root_url = nil
362
+ end
363
+ end
364
+ end
365
+
366
+ it "should not get stuck rendering each request as pdf" do
367
+ mock_app
368
+ # false by default. No requests.
369
+ expect(@app.send(:rendering_pdf?)).to be_false
370
+
371
+ # Remain false on a normal request
372
+ get 'http://www.example.org/public/file'
373
+ expect(@app.send(:rendering_pdf?)).to be_false
374
+
375
+ # Return true on a pdf request.
376
+ get 'http://www.example.org/public/file.pdf'
377
+ expect(@app.send(:rendering_pdf?)).to be_true
378
+
379
+ # Restore to false on any non-pdf request.
380
+ get 'http://www.example.org/public/file'
381
+ expect(@app.send(:rendering_pdf?)).to be_false
382
+ end
383
+
384
+ end