roadie 2.4.3 → 3.0.0.pre1

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