md-roadie 2.4.2.md.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.autotest +10 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +22 -0
  4. data/.yardopts +1 -0
  5. data/Appraisals +15 -0
  6. data/Changelog.md +185 -0
  7. data/Gemfile +11 -0
  8. data/Guardfile +8 -0
  9. data/MIT-LICENSE +20 -0
  10. data/README.md +310 -0
  11. data/Rakefile +30 -0
  12. data/autotest/discover.rb +1 -0
  13. data/gemfiles/rails_3.0.gemfile +7 -0
  14. data/gemfiles/rails_3.0.gemfile.lock +123 -0
  15. data/gemfiles/rails_3.1.gemfile +7 -0
  16. data/gemfiles/rails_3.1.gemfile.lock +126 -0
  17. data/gemfiles/rails_3.2.gemfile +7 -0
  18. data/gemfiles/rails_3.2.gemfile.lock +124 -0
  19. data/gemfiles/rails_4.0.gemfile +7 -0
  20. data/gemfiles/rails_4.0.gemfile.lock +119 -0
  21. data/lib/roadie.rb +79 -0
  22. data/lib/roadie/action_mailer_extensions.rb +95 -0
  23. data/lib/roadie/asset_pipeline_provider.rb +28 -0
  24. data/lib/roadie/asset_provider.rb +62 -0
  25. data/lib/roadie/css_file_not_found.rb +22 -0
  26. data/lib/roadie/filesystem_provider.rb +74 -0
  27. data/lib/roadie/inliner.rb +251 -0
  28. data/lib/roadie/railtie.rb +39 -0
  29. data/lib/roadie/selector.rb +50 -0
  30. data/lib/roadie/style_declaration.rb +42 -0
  31. data/lib/roadie/version.rb +3 -0
  32. data/md-roadie.gemspec +36 -0
  33. data/spec/fixtures/app/assets/stylesheets/integration.css +10 -0
  34. data/spec/fixtures/public/stylesheets/integration.css +10 -0
  35. data/spec/fixtures/views/integration_mailer/marketing.html.erb +2 -0
  36. data/spec/fixtures/views/integration_mailer/notification.html.erb +8 -0
  37. data/spec/fixtures/views/integration_mailer/notification.text.erb +6 -0
  38. data/spec/integration_spec.rb +110 -0
  39. data/spec/lib/roadie/action_mailer_extensions_spec.rb +227 -0
  40. data/spec/lib/roadie/asset_pipeline_provider_spec.rb +65 -0
  41. data/spec/lib/roadie/css_file_not_found_spec.rb +29 -0
  42. data/spec/lib/roadie/filesystem_provider_spec.rb +94 -0
  43. data/spec/lib/roadie/inliner_spec.rb +591 -0
  44. data/spec/lib/roadie/selector_spec.rb +55 -0
  45. data/spec/lib/roadie/style_declaration_spec.rb +49 -0
  46. data/spec/lib/roadie_spec.rb +101 -0
  47. data/spec/shared_examples/asset_provider_examples.rb +11 -0
  48. data/spec/spec_helper.rb +69 -0
  49. data/spec/support/anonymous_mailer.rb +21 -0
  50. data/spec/support/change_url_options.rb +5 -0
  51. data/spec/support/have_attribute_matcher.rb +28 -0
  52. data/spec/support/have_node_matcher.rb +19 -0
  53. data/spec/support/have_selector_matcher.rb +6 -0
  54. data/spec/support/have_styling_matcher.rb +25 -0
  55. data/spec/support/parse_styling.rb +25 -0
  56. metadata +318 -0
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+ require 'shared_examples/asset_provider_examples'
3
+
4
+ module Roadie
5
+ describe AssetPipelineProvider do
6
+ let(:provider) { AssetPipelineProvider.new }
7
+
8
+ it_behaves_like AssetProvider
9
+
10
+ it "has a configurable prefix" do
11
+ AssetPipelineProvider.new("/prefix").prefix.should == "/prefix"
12
+ end
13
+
14
+ it 'has a prefix of "/assets" by default' do
15
+ provider.prefix.should == "/assets"
16
+ end
17
+
18
+ describe "#find(file)" do
19
+ let(:pipeline) { double("Rails asset pipeline") }
20
+ before(:each) { Roadie.app.stub(:assets => pipeline) }
21
+
22
+ def expect_pipeline_access(name, returning = '')
23
+ pipeline.should_receive(:[]).with(name).and_return(returning)
24
+ end
25
+
26
+ it "loads files matching the target names in Rails assets" do
27
+ expect_pipeline_access('foo', 'contents of foo')
28
+ expect_pipeline_access('foo.css', 'contents of foo.css')
29
+
30
+ provider.find('foo').should == 'contents of foo'
31
+ provider.find('foo.css').should == 'contents of foo.css'
32
+ end
33
+
34
+ it "strips the contents" do
35
+ expect_pipeline_access('foo', " contents \n ")
36
+ provider.find('foo').should == "contents"
37
+ end
38
+
39
+ it "removes the prefix from the filename" do
40
+ expect_pipeline_access('foo')
41
+ expect_pipeline_access('path/to/foo')
42
+ expect_pipeline_access('bar')
43
+
44
+ provider = AssetPipelineProvider.new("/prefix")
45
+ provider.find('/prefix/foo')
46
+ provider.find('/prefix/path/to/foo')
47
+ provider.find('prefix/bar')
48
+ end
49
+
50
+ it "cleans up double slashes from the path" do
51
+ expect_pipeline_access('path/to/foo')
52
+
53
+ provider = AssetPipelineProvider.new("/prefix/")
54
+ provider.find('/prefix/path/to//foo')
55
+ end
56
+
57
+ it "raises a Roadie::CSSFileNotFound error when the file could not be found" do
58
+ expect_pipeline_access('not_here', nil)
59
+ expect {
60
+ provider.find('not_here')
61
+ }.to raise_error(Roadie::CSSFileNotFound, /not_here/)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ module Roadie
4
+ describe CSSFileNotFound do
5
+ it "is initialized with a filename" do
6
+ CSSFileNotFound.new('file.css').filename.should == 'file.css'
7
+ end
8
+
9
+ it "can be initialized with the guess the filename was based on" do
10
+ CSSFileNotFound.new('file.css', :file).guess.should == :file
11
+ end
12
+
13
+ it "has a nil guess when no guess was specified" do
14
+ CSSFileNotFound.new('').guess.should be_nil
15
+ end
16
+
17
+ context "without a guess" do
18
+ it "has a message with the wanted filename" do
19
+ CSSFileNotFound.new('style.css').message.should == 'Could not find style.css'
20
+ end
21
+ end
22
+
23
+ context "with a guess" do
24
+ it "has a message with the wanted filename and the guess" do
25
+ CSSFileNotFound.new('style.css', :style).message.should == 'Could not find style.css (guessed from :style)'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+ require 'shared_examples/asset_provider_examples'
3
+ require 'tmpdir'
4
+
5
+ module Roadie
6
+ describe FilesystemProvider do
7
+ let(:provider) { FilesystemProvider.new }
8
+
9
+ it_behaves_like AssetProvider
10
+
11
+ it "has a configurable prefix" do
12
+ FilesystemProvider.new("/prefix").prefix.should == "/prefix"
13
+ end
14
+
15
+ it "has a configurable path" do
16
+ path = Pathname.new("/path")
17
+ FilesystemProvider.new('', path).path.should == path
18
+ end
19
+
20
+ it "bases the path on the project root if passed a string with a relative path" do
21
+ FilesystemProvider.new('', "foo/bar").path.should == Roadie.app.root.join("foo", "bar")
22
+ FilesystemProvider.new('', "/foo/bar").path.should == Pathname.new("/foo/bar")
23
+ end
24
+
25
+ it 'has a path of "<root>/public/stylesheets" by default' do
26
+ FilesystemProvider.new.path.should == Roadie.app.root.join('public', 'stylesheets')
27
+ end
28
+
29
+ it 'has a prefix of "/stylesheets" by default' do
30
+ FilesystemProvider.new.prefix.should == "/stylesheets"
31
+ end
32
+
33
+ describe "#find(file)" do
34
+ around(:each) do |example|
35
+ Dir.mktmpdir do |path|
36
+ Dir.chdir(path) { example.run }
37
+ end
38
+ end
39
+
40
+ let(:provider) { FilesystemProvider.new('/prefix', Pathname.new('.')) }
41
+
42
+ def create_file(name, contents = '')
43
+ Pathname.new(name).tap do |path|
44
+ path.dirname.mkpath unless path.dirname.directory?
45
+ path.open('w') { |file| file << contents }
46
+ end
47
+ end
48
+
49
+ it "loads files from the filesystem" do
50
+ create_file('foo.css', 'contents of foo.css')
51
+ provider.find('foo.css').should == 'contents of foo.css'
52
+ end
53
+
54
+ it "removes the prefix" do
55
+ create_file('foo.css', 'contents of foo.css')
56
+ provider.find('/prefix/foo.css').should == 'contents of foo.css'
57
+ provider.find('prefix/foo.css').should == 'contents of foo.css'
58
+ end
59
+
60
+ it 'tries the filename with a ".css" extension if it does not exist' do
61
+ create_file('bar', 'in bare bar')
62
+ create_file('bar.css', 'in bar.css')
63
+ create_file('foo.css', 'in foo.css')
64
+
65
+ provider.find('bar').should == 'in bare bar'
66
+ provider.find('foo').should == 'in foo.css'
67
+ end
68
+
69
+ it "strips the contents" do
70
+ create_file('foo.css', " contents \n ")
71
+ provider.find('foo.css').should == "contents"
72
+ end
73
+
74
+ it "supports nested directories" do
75
+ create_file('path/to/foo.css')
76
+ create_file('path/from/bar.css')
77
+
78
+ provider.find('path/to/foo.css')
79
+ provider.find('path/from/bar.css')
80
+ end
81
+
82
+ it "works with double slashes in the path" do
83
+ create_file('path/to/foo.css')
84
+ provider.find('path/to//foo.css')
85
+ end
86
+
87
+ it "raises a Roadie::CSSFileNotFound error when the file could not be found" do
88
+ expect {
89
+ provider.find('not_here.css')
90
+ }.to raise_error(Roadie::CSSFileNotFound, /not_here/)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,591 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+
4
+ describe Roadie::Inliner do
5
+ let(:provider) { double("asset provider", :all => '') }
6
+
7
+ def use_css(css)
8
+ provider.stub(:all).with(['global.css']).and_return(css)
9
+ end
10
+
11
+ def raw_rendering(html, options = {})
12
+ url_options = options.fetch(:url_options, {:host => 'example.com'})
13
+ document_options = options.fetch(:document_options) { {} }
14
+ after_inlining_handler = options[:after_inlining_handler]
15
+ Roadie::Inliner.new(provider, ['global.css'], html, url_options, after_inlining_handler, document_options).execute
16
+ end
17
+
18
+ def rendering(html, options = {})
19
+ Nokogiri::HTML.parse(raw_rendering(html, options))
20
+ end
21
+
22
+ describe "initialization" do
23
+ it "warns about asset_path_prefix being non-functional" do
24
+ expect {
25
+ Roadie::Inliner.new(provider, [], '', :asset_path_prefix => 'foo')
26
+ }.to raise_error(ArgumentError, /asset_path_prefix/)
27
+ end
28
+ end
29
+
30
+ describe "inlining styles" do
31
+ before(:each) do
32
+ # Make sure to have some css even when we don't specify any
33
+ # We have specific tests for when this is nil
34
+ use_css ''
35
+ end
36
+
37
+ it "inlines simple attributes" do
38
+ use_css 'p { color: green }'
39
+ rendering('<p></p>').should have_styling('color' => 'green')
40
+ end
41
+
42
+ it "inlines browser-prefixed attributes" do
43
+ use_css 'p { -vendor-color: green }'
44
+ rendering('<p></p>').should have_styling('-vendor-color' => 'green')
45
+ end
46
+
47
+ it "inlines CSS3 attributes" do
48
+ use_css 'p { border-radius: 2px; }'
49
+ rendering('<p></p>').should have_styling('border-radius' => '2px')
50
+ end
51
+
52
+ it "keeps the order of the styles that are inlined" do
53
+ use_css 'h1 { padding: 2px; margin: 5px; }'
54
+ rendering('<h1></h1>').should have_styling([['padding', '2px'], ['margin', '5px']])
55
+ end
56
+
57
+ it "combines multiple selectors into one" do
58
+ use_css 'p { color: green; }
59
+ .tip { float: right; }'
60
+ rendering('<p class="tip"></p>').should have_styling([['color', 'green'], ['float', 'right']])
61
+ end
62
+
63
+ it "uses the attributes with the highest specificity when conflicts arises" do
64
+ use_css "p { color: red; }
65
+ .safe { color: green; }"
66
+ rendering('<p class="safe"></p>').should have_styling('color' => 'green')
67
+ end
68
+
69
+ it "sorts styles by specificity order" do
70
+ use_css 'p { margin: 2px; }
71
+ #big { margin: 10px; }
72
+ .down { margin-bottom: 5px; }'
73
+
74
+ rendering('<p class="down"></p>').should have_styling([
75
+ ['margin', '2px'], ['margin-bottom', '5px']
76
+ ])
77
+
78
+ rendering('<p class="down" id="big"></p>').should have_styling([
79
+ ['margin-bottom', '5px'], ['margin', '10px']
80
+ ])
81
+ end
82
+
83
+ it "supports multiple selectors for the same rules" do
84
+ use_css 'p, a { color: green; }'
85
+ rendering('<p></p><a></a>').tap do |document|
86
+ document.should have_styling('color' => 'green').at_selector('p')
87
+ document.should have_styling('color' => 'green').at_selector('a')
88
+ end
89
+ end
90
+
91
+ it "keeps !important properties" do
92
+ use_css "a { text-decoration: underline !important; }
93
+ a.hard-to-spot { text-decoration: none; }"
94
+ rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline !important')
95
+ end
96
+
97
+ it "combines with already present inline styles" do
98
+ use_css "p { color: green }"
99
+ rendering('<p style="font-size: 1.1em"></p>').should have_styling([['color', 'green'], ['font-size', '1.1em']])
100
+ end
101
+
102
+ it "does not touch already present inline styles" do
103
+ use_css "p { color: red }"
104
+ rendering('<p style="color: green"></p>').should have_styling([['color', 'red'], ['color', 'green']])
105
+ end
106
+
107
+ it "does not apply link and dynamic pseudo selectors" do
108
+ use_css "
109
+ p:active { color: red }
110
+ p:focus { color: red }
111
+ p:hover { color: red }
112
+ p:link { color: red }
113
+ p:target { color: red }
114
+ p:visited { color: red }
115
+
116
+ p.active { width: 100%; }
117
+ "
118
+ rendering('<p class="active"></p>').should have_styling('width' => '100%')
119
+ end
120
+
121
+ it "does not crash on any pseudo element selectors" do
122
+ use_css "
123
+ p.some-element { width: 100%; }
124
+ p::some-element { color: red; }
125
+ "
126
+ rendering('<p class="some-element"></p>').should have_styling('width' => '100%')
127
+ end
128
+
129
+ it "works with nth-child" do
130
+ use_css "
131
+ p { color: red; }
132
+ p:nth-child(2n) { color: green; }
133
+ "
134
+ rendering("
135
+ <p class='one'></p>
136
+ <p class='two'></p>
137
+ ").should have_styling('color' => 'green').at_selector('.two')
138
+ end
139
+
140
+ it "ignores selectors with @" do
141
+ use_css '@keyframes progress-bar-stripes {
142
+ from {
143
+ background-position: 40px 0;
144
+ }
145
+ to {
146
+ background-position: 0 0;
147
+ }
148
+ }'
149
+ expect { rendering('<p></p>') }.not_to raise_error
150
+ end
151
+
152
+ it 'parses fragment when given fragment document option' do
153
+ raw_rendering('<p></p>', :document_options => { :fragment => true }).should_not match(/<html>/)
154
+ end
155
+
156
+ it 'ignores HTML comments and CDATA sections' do
157
+ # TinyMCE posts invalid CSS. We support that just to be pragmatic.
158
+ use_css %(<![CDATA[
159
+ <!--
160
+ p { color: green }
161
+ -->
162
+ ]]>)
163
+ expect { rendering '<p></p>' }.not_to raise_error
164
+
165
+ use_css %(
166
+ <!--
167
+ <![CDATA[
168
+ <![CDATA[
169
+ span { color: red }
170
+ ]]>
171
+ -->
172
+ )
173
+ expect { rendering '<p></p>' }.not_to raise_error
174
+ end
175
+
176
+ it "does not pick up scripts generating styles" do
177
+ expect {
178
+ rendering <<-HTML
179
+ <script>
180
+ var color = "red";
181
+ document.write("<style type='text/css'>p { color: " + color + "; }</style>");
182
+ </script>
183
+ HTML
184
+ }.not_to raise_error
185
+ end
186
+
187
+ describe "inline <style> element" do
188
+ it "is used for inlined styles" do
189
+ rendering(<<-HTML).should have_styling([['color', 'green'], ['font-size', '1.1em']])
190
+ <html>
191
+ <head>
192
+ <style type="text/css">p { color: green; }</style>
193
+ </head>
194
+ <body>
195
+ <p>Hello World</p>
196
+ <style type="text/css">p { font-size: 1.1em; }</style>
197
+ </body>
198
+ </html>
199
+ HTML
200
+ end
201
+
202
+ it "is removed" do
203
+ rendering(<<-HTML).should_not have_selector('style')
204
+ <html>
205
+ <head>
206
+ <style type="text/css">p { color: green; }</style>
207
+ </head>
208
+ <body>
209
+ <style type="text/css">p { font-size: 1.1em; }</style>
210
+ </body>
211
+ </html>
212
+ HTML
213
+ end
214
+
215
+ it "is not touched when data-immutable is set" do
216
+ document = rendering <<-HTML
217
+ <style type="text/css" data-immutable>p { color: red; }</style>
218
+ <p></p>
219
+ HTML
220
+ document.should have_selector('style[data-immutable]')
221
+ document.should_not have_styling('color' => 'red')
222
+ end
223
+
224
+ it "is not touched when for print media" do
225
+ document = rendering <<-HTML
226
+ <style type="text/css" media="print">p { color: red; }</style>
227
+ <p></p>
228
+ HTML
229
+ document.should have_selector('style[media=print]')
230
+ document.should_not have_styling('color' => 'red').at_selector('p')
231
+ end
232
+
233
+ it "is still inlined when no external css rules are defined" do
234
+ # This is just testing that the main code paths are still active even
235
+ # when css is set to nil
236
+ use_css nil
237
+ rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p')
238
+ <style type="text/css">p { color: green; }</style>
239
+ <p>Hello World</p>
240
+ HTML
241
+ end
242
+
243
+ it "is not touched when inside a SVG element" do
244
+ expect {
245
+ rendering <<-HTML
246
+ <p>Hello World</p>
247
+ <svg>
248
+ <style>This is not parseable by the CSS parser!</style>
249
+ </svg>
250
+ HTML
251
+ }.to_not raise_error
252
+ end
253
+ end
254
+ end
255
+
256
+ describe "linked stylesheets" do
257
+ def fake_file(name, contents)
258
+ provider.should_receive(:find).with(name).and_return(contents)
259
+ end
260
+
261
+ it "inlines styles from the linked stylesheet" do
262
+ fake_file('/assets/green_paragraphs.css', 'p { color: green; }')
263
+
264
+ rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p')
265
+ <html>
266
+ <head>
267
+ <link rel="stylesheet" href="/assets/green_paragraphs.css">
268
+ </head>
269
+ <body>
270
+ <p></p>
271
+ </body>
272
+ </html>
273
+ HTML
274
+ end
275
+
276
+ it "inlines styles from the linked stylesheet in subdirectory" do
277
+ fake_file('/assets/subdirectory/red_paragraphs.css', 'p { color: red; }')
278
+
279
+ rendering(<<-HTML).should have_styling('color' => 'red').at_selector('p')
280
+ <html>
281
+ <head>
282
+ <link rel="stylesheet" href="/assets/subdirectory/red_paragraphs.css">
283
+ </head>
284
+ <body>
285
+ <p></p>
286
+ </body>
287
+ </html>
288
+ HTML
289
+ end
290
+
291
+ it "inlines styles from more than one linked stylesheet" do
292
+ fake_file('/assets/large_purple_paragraphs.css', 'p { font-size: 18px; color: purple; }')
293
+ fake_file('/assets/green_paragraphs.css', 'p { color: green; }')
294
+
295
+ html = <<-HTML
296
+ <html>
297
+ <head>
298
+ <link rel="stylesheet" href="/assets/large_purple_paragraphs.css">
299
+ <link rel="stylesheet" href="/assets/green_paragraphs.css">
300
+ </head>
301
+ <body>
302
+ <p></p>
303
+ </body>
304
+ </html>
305
+ HTML
306
+
307
+ rendering(html).should have_styling([
308
+ ['font-size', '18px'],
309
+ ['color', 'green'],
310
+ ]).at_selector('p')
311
+ end
312
+
313
+ it "removes the stylesheet links from the DOM" do
314
+ provider.stub(:find => '')
315
+ rendering(<<-HTML).should_not have_selector('link')
316
+ <html>
317
+ <head>
318
+ <link rel="stylesheet" href="/assets/green_paragraphs.css">
319
+ <link rel="stylesheet" href="/assets/large_purple_paragraphs.css">
320
+ </head>
321
+ <body>
322
+ </body>
323
+ </html>
324
+ HTML
325
+ end
326
+
327
+ context "when stylesheet is for print media" do
328
+ it "does not inline the stylesheet" do
329
+ rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p')
330
+ <html>
331
+ <head>
332
+ <link rel="stylesheet" href="/assets/green_paragraphs.css" media="print">
333
+ </head>
334
+ <body>
335
+ <p></p>
336
+ </body>
337
+ </html>
338
+ HTML
339
+ end
340
+
341
+ it "does not remove the links" do
342
+ rendering(<<-HTML).should have_selector('link')
343
+ <html>
344
+ <head>
345
+ <link rel="stylesheet" href="/assets/green_paragraphs.css" media="print">
346
+ </head>
347
+ <body>
348
+ </body>
349
+ </html>
350
+ HTML
351
+ end
352
+ end
353
+
354
+ context "when stylesheet is marked as immutable" do
355
+ it "does not inline the stylesheet" do
356
+ rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p')
357
+ <html>
358
+ <head>
359
+ <link rel="stylesheet" href="/assets/green_paragraphs.css" data-immutable="true">
360
+ </head>
361
+ <body>
362
+ <p></p>
363
+ </body>
364
+ </html>
365
+ HTML
366
+ end
367
+
368
+ it "does not remove link" do
369
+ rendering(<<-HTML).should have_selector('link')
370
+ <html>
371
+ <head>
372
+ <link rel="stylesheet" href="/assets/green_paragraphs.css" data-immutable="true">
373
+ </head>
374
+ <body>
375
+ </body>
376
+ </html>
377
+ HTML
378
+ end
379
+ end
380
+
381
+ context "when stylesheet link uses an absolute URL" do
382
+ it "does not inline the stylesheet" do
383
+ rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p')
384
+ <html>
385
+ <head>
386
+ <link rel="stylesheet" href="http://www.example.com/green_paragraphs.css">
387
+ </head>
388
+ <body>
389
+ <p></p>
390
+ </body>
391
+ </html>
392
+ HTML
393
+ end
394
+
395
+ it "does not remove link" do
396
+ rendering(<<-HTML).should have_selector('link')
397
+ <html>
398
+ <head>
399
+ <link rel="stylesheet" href="http://www.example.com/green_paragraphs.css">
400
+ </head>
401
+ <body>
402
+ </body>
403
+ </html>
404
+ HTML
405
+ end
406
+ end
407
+
408
+ context "stylesheet cannot be found on disk" do
409
+ it "raises an error" do
410
+ html = <<-HTML
411
+ <html>
412
+ <head>
413
+ <link rel="stylesheet" href="/assets/not_found.css">
414
+ </head>
415
+ <body>
416
+ </body>
417
+ </html>
418
+ HTML
419
+
420
+ expect { rendering(html) }.to raise_error do |error|
421
+ error.should be_a(Roadie::CSSFileNotFound)
422
+ error.filename.should == Roadie.app.assets['not_found.css']
423
+ error.guess.should == '/assets/not_found.css'
424
+ end
425
+ end
426
+ end
427
+
428
+ context "link element is not for a stylesheet" do
429
+ it "is ignored" do
430
+ html = <<-HTML
431
+ <html>
432
+ <head>
433
+ <link rel="not_stylesheet" href="/assets/green_paragraphs.css">
434
+ </head>
435
+ <body>
436
+ <p></p>
437
+ </body>
438
+ </html>
439
+ HTML
440
+ rendering(html).tap do |document|
441
+ document.should_not have_styling('color' => 'green').at_selector('p')
442
+ document.should have_selector('link')
443
+ end
444
+ end
445
+ end
446
+ end
447
+
448
+ describe "making urls absolute" do
449
+ it "works on image sources" do
450
+ rendering('<img src="/images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg')
451
+ rendering('<img src="../images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg')
452
+ rendering('<img src="foo.jpg" />').should have_attribute('src' => 'http://example.com/foo.jpg')
453
+ end
454
+
455
+ it "does not touch image sources that are already absolute" do
456
+ rendering('<img src="http://other.example.org/images/foo.jpg" />').should have_attribute('src' => 'http://other.example.org/images/foo.jpg')
457
+ end
458
+
459
+ it "works on inlined style attributes" do
460
+ rendering('<p style="background: url(/paper.png)"></p>').should have_styling('background' => 'url(http://example.com/paper.png)')
461
+ rendering('<p style="background: url(&quot;/paper.png&quot;)"></p>').should have_styling('background' => 'url("http://example.com/paper.png")')
462
+ end
463
+
464
+ it "works on external style declarations" do
465
+ use_css "p { background-image: url(/paper.png); }
466
+ table { background-image: url('/paper.png'); }
467
+ div { background-image: url(\"/paper.png\"); }"
468
+ rendering('<p></p>').should have_styling('background-image' => 'url(http://example.com/paper.png)')
469
+ rendering('<table></table>').should have_styling('background-image' => "url('http://example.com/paper.png')")
470
+ rendering('<div></div>').should have_styling('background-image' => 'url("http://example.com/paper.png")')
471
+ end
472
+
473
+ it "does not touch style urls that are already absolute" do
474
+ external_url = 'url(http://other.example.org/paper.png)'
475
+ use_css "p { background-image: #{external_url}; }"
476
+ rendering('<p></p>').should have_styling('background-image' => external_url)
477
+ rendering(%(<div style="background-image: #{external_url}"></div>)).should have_styling('background-image' => external_url)
478
+ end
479
+
480
+ it "does not touch the urls when no url options are defined" do
481
+ use_css "img { background: url(/a.jpg); }"
482
+ rendering('<img src="/b.jpg" />', :url_options => nil).tap do |document|
483
+ document.should have_attribute('src' => '/b.jpg').at_selector('img')
484
+ document.should have_styling('background' => 'url(/a.jpg)')
485
+ end
486
+ end
487
+
488
+ it "supports port and protocol settings" do
489
+ use_css "img { background: url(/a.jpg); }"
490
+ rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https', :port => '8080'}).tap do |document|
491
+ document.should have_attribute('src' => 'https://example.com:8080/b.jpg').at_selector('img')
492
+ document.should have_styling('background' => 'url(https://example.com:8080/a.jpg)')
493
+ end
494
+ end
495
+
496
+ # This case was happening for some users when emails were rendered as part
497
+ # of the request cycle. I do not know it we *really* should accept these
498
+ # values, but it looks like Rails do accept it so we might as well do it
499
+ # too.
500
+ it "supports protocol settings with additional tokens" do
501
+ use_css "img { background: url(/a.jpg); }"
502
+ rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https://'}).tap do |document|
503
+ document.should have_attribute('src' => 'https://example.com/b.jpg').at_selector('img')
504
+ document.should have_styling('background' => 'url(https://example.com/a.jpg)')
505
+ end
506
+ end
507
+
508
+ it "does not touch data: URIs" do
509
+ use_css "div { background: url(data:abcdef); }"
510
+ rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)')
511
+ end
512
+ end
513
+
514
+ describe "custom converter" do
515
+ let(:html) { '<div id="foo"></div>' }
516
+
517
+ it "is invoked" do
518
+ after_inlining_handler = double("converter")
519
+ after_inlining_handler.should_receive(:call).with(anything)
520
+ rendering(html, :after_inlining_handler => after_inlining_handler)
521
+ end
522
+
523
+ it "modifies the document using lambda" do
524
+ after_inlining_handler = lambda {|d| d.css("#foo").first["class"] = "bar"}
525
+ rendering(html, :after_inlining_handler => after_inlining_handler).css("#foo").first["class"].should == "bar"
526
+ end
527
+
528
+ it "modifies the document using object" do
529
+ klass = Class.new do
530
+ def call(d)
531
+ d.css("#foo").first["class"] = "bar"
532
+ end
533
+ end
534
+ after_inlining_handler = klass.new
535
+ rendering(html, :after_inlining_handler => after_inlining_handler).css("#foo").first["class"].should == "bar"
536
+ end
537
+ end
538
+
539
+ describe "inserting tags" do
540
+ it "inserts a doctype if not present" do
541
+ rendering('<html><body></body></html>').to_xml.should include('<!DOCTYPE ')
542
+ rendering('<!DOCTYPE html><html><body></body></html>').to_xml.should_not match(/(DOCTYPE.*?){2}/)
543
+ end
544
+
545
+ it "sets xmlns of <html> to that of XHTML" do
546
+ rendering('<html><body></body></html>').should have_node('html').with_attributes("xmlns" => "http://www.w3.org/1999/xhtml")
547
+ end
548
+
549
+ it "inserts basic html structure if not present" do
550
+ rendering('<h1>Hey!</h1>').should have_selector('html > head + body > h1')
551
+ end
552
+
553
+ it "inserts <head> if not present" do
554
+ rendering('<html><body></body></html>').should have_selector('html > head + body')
555
+ end
556
+
557
+ it "inserts meta tag describing content-type" do
558
+ rendering('<html><head></head><body></body></html>').tap do |document|
559
+ document.should have_selector('head meta[http-equiv="Content-Type"]')
560
+ document.css('head meta[http-equiv="Content-Type"]').first['content'].should == 'text/html; charset=UTF-8'
561
+ end
562
+ end
563
+
564
+ it "does not insert duplicate meta tags describing content-type" do
565
+ rendering(<<-HTML).to_html.scan('meta').should have(1).item
566
+ <html>
567
+ <head>
568
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
569
+ </head>
570
+ </html>
571
+ HTML
572
+ end
573
+ end
574
+
575
+ describe "css url regex" do
576
+ it "parses css urls" do
577
+ {
578
+ %{url(/foo.jpg)} => '/foo.jpg',
579
+ %{url("/foo.jpg")} => '/foo.jpg',
580
+ %{url('/foo.jpg')} => '/foo.jpg',
581
+ %{url(http://localhost/foo.jpg)} => 'http://localhost/foo.jpg',
582
+ %{url("http://localhost/foo.jpg")} => 'http://localhost/foo.jpg',
583
+ %{url('http://localhost/foo.jpg')} => 'http://localhost/foo.jpg',
584
+ %{url(/andromeda_(galaxy).jpg)} => '/andromeda_(galaxy).jpg',
585
+ }.each do |raw, expected|
586
+ raw =~ Roadie::Inliner::CSS_URL_REGEXP
587
+ $2.should == expected
588
+ end
589
+ end
590
+ end
591
+ end