roadie 2.4.3 → 3.0.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.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +10 -14
  4. data/.yardopts +1 -1
  5. data/Changelog.md +38 -5
  6. data/Gemfile +3 -4
  7. data/Guardfile +12 -1
  8. data/README.md +168 -164
  9. data/Rakefile +2 -19
  10. data/lib/roadie.rb +15 -68
  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 +30 -60
  16. data/lib/roadie/inliner.rb +72 -217
  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 +71 -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 +43 -18
  24. data/lib/roadie/style_attribute_builder.rb +25 -0
  25. data/lib/roadie/style_block.rb +32 -0
  26. data/lib/roadie/style_property.rb +93 -0
  27. data/lib/roadie/stylesheet.rb +65 -0
  28. data/lib/roadie/upgrade_guide.rb +36 -0
  29. data/lib/roadie/url_generator.rb +126 -0
  30. data/lib/roadie/url_rewriter.rb +84 -0
  31. data/lib/roadie/version.rb +1 -1
  32. data/roadie.gemspec +8 -11
  33. data/spec/fixtures/big_em.css +1 -0
  34. data/spec/fixtures/stylesheets/green.css +1 -0
  35. data/spec/integration_spec.rb +125 -95
  36. data/spec/lib/roadie/asset_scanner_spec.rb +153 -0
  37. data/spec/lib/roadie/css_not_found_spec.rb +17 -0
  38. data/spec/lib/roadie/document_spec.rb +123 -0
  39. data/spec/lib/roadie/filesystem_provider_spec.rb +44 -68
  40. data/spec/lib/roadie/inliner_spec.rb +105 -537
  41. data/spec/lib/roadie/markup_improver_spec.rb +78 -0
  42. data/spec/lib/roadie/null_provider_spec.rb +21 -0
  43. data/spec/lib/roadie/null_url_rewriter_spec.rb +19 -0
  44. data/spec/lib/roadie/provider_list_spec.rb +89 -0
  45. data/spec/lib/roadie/selector_spec.rb +15 -10
  46. data/spec/lib/roadie/style_attribute_builder_spec.rb +29 -0
  47. data/spec/lib/roadie/style_block_spec.rb +35 -0
  48. data/spec/lib/roadie/style_property_spec.rb +82 -0
  49. data/spec/lib/roadie/stylesheet_spec.rb +41 -0
  50. data/spec/lib/roadie/test_provider_spec.rb +29 -0
  51. data/spec/lib/roadie/url_generator_spec.rb +121 -0
  52. data/spec/lib/roadie/url_rewriter_spec.rb +79 -0
  53. data/spec/shared_examples/asset_provider.rb +11 -0
  54. data/spec/shared_examples/url_rewriter.rb +23 -0
  55. data/spec/spec_helper.rb +6 -60
  56. data/spec/support/have_attribute_matcher.rb +2 -2
  57. data/spec/support/have_node_matcher.rb +4 -4
  58. data/spec/support/have_selector_matcher.rb +3 -3
  59. data/spec/support/have_styling_matcher.rb +51 -15
  60. data/spec/support/test_provider.rb +13 -0
  61. metadata +86 -175
  62. data/Appraisals +0 -15
  63. data/gemfiles/rails_3.0.gemfile +0 -7
  64. data/gemfiles/rails_3.0.gemfile.lock +0 -123
  65. data/gemfiles/rails_3.1.gemfile +0 -7
  66. data/gemfiles/rails_3.1.gemfile.lock +0 -126
  67. data/gemfiles/rails_3.2.gemfile +0 -7
  68. data/gemfiles/rails_3.2.gemfile.lock +0 -124
  69. data/gemfiles/rails_4.0.gemfile +0 -7
  70. data/gemfiles/rails_4.0.gemfile.lock +0 -119
  71. data/lib/roadie/action_mailer_extensions.rb +0 -95
  72. data/lib/roadie/asset_pipeline_provider.rb +0 -28
  73. data/lib/roadie/css_file_not_found.rb +0 -22
  74. data/lib/roadie/railtie.rb +0 -39
  75. data/lib/roadie/style_declaration.rb +0 -42
  76. data/spec/fixtures/app/assets/stylesheets/integration.css +0 -10
  77. data/spec/fixtures/public/stylesheets/integration.css +0 -10
  78. data/spec/fixtures/views/integration_mailer/marketing.html.erb +0 -2
  79. data/spec/fixtures/views/integration_mailer/notification.html.erb +0 -8
  80. data/spec/fixtures/views/integration_mailer/notification.text.erb +0 -6
  81. data/spec/lib/roadie/action_mailer_extensions_spec.rb +0 -227
  82. data/spec/lib/roadie/asset_pipeline_provider_spec.rb +0 -65
  83. data/spec/lib/roadie/css_file_not_found_spec.rb +0 -29
  84. data/spec/lib/roadie/style_declaration_spec.rb +0 -49
  85. data/spec/lib/roadie_spec.rb +0 -101
  86. data/spec/shared_examples/asset_provider_examples.rb +0 -11
  87. data/spec/support/anonymous_mailer.rb +0 -21
  88. data/spec/support/change_url_options.rb +0 -5
  89. 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
+ expect(scanner.dom).to eq(dom)
15
+ expect(scanner.asset_provider).to eq(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
+ expect(scanner.find_css).to eq([])
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
+ expect(stylesheets).to have(2).stylesheets
42
+ expect(stylesheets[0].to_s).to include("green")
43
+ expect(stylesheets[1].to_s).to include("red")
44
+
45
+ expect(stylesheets.first.name).to eq("(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
+ expect(scanner.find_css).to have(1).stylesheet
59
+ end
60
+
61
+ it "finds referenced stylesheets through the provider" do
62
+ stylesheet = double "A stylesheet"
63
+ expect(provider).to 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
+ expect(scanner.find_css).to eq([stylesheet])
69
+ end
70
+
71
+ it "ignores referenced print stylesheets" do
72
+ dom = dom_fragment %(<link rel="stylesheet" href="/error.css" media="print">)
73
+ expect(provider).not_to receive(:find_stylesheet!)
74
+
75
+ scanner = AssetScanner.new dom, provider
76
+
77
+ expect(scanner.find_css).to eq([])
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
+ expect(provider).not_to receive(:find_stylesheet!)
83
+
84
+ scanner = AssetScanner.new dom, provider
85
+
86
+ expect(scanner.find_css).to eq([])
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
+ expect(stylesheet.to_s).to include("green")
101
+ expect(stylesheet.to_s).not_to include("!--")
102
+ expect(stylesheet.to_s).not_to 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
+ expect(scanner.find_css).to eq([])
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
+ expect(stylesheets).to have(2).stylesheets
140
+ expect(stylesheets[0].to_s).to include("span")
141
+ expect(stylesheets[1].to_s).to include("body")
142
+
143
+ expect(dom).to have_selector("html > head > title")
144
+ expect(dom).to have_selector("html > body > style[data-roadie-ignore]")
145
+ expect(dom).to have_selector("link[data-roadie-ignore]")
146
+ expect(dom).to have_selector("link[media=print]")
147
+
148
+ expect(dom).not_to have_selector("html > head > style")
149
+ expect(dom).not_to have_selector("html > head > link[href='/some/url.css']")
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,17 @@
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
+ expect(error.css_name).to eq('style.css')
8
+ expect(error.message).to eq('Could not find stylesheet "style.css"')
9
+ end
10
+
11
+ it "can be initialized with an extra message" do
12
+ expect(CssNotFound.new('file.css', "directory is missing").message).to eq(
13
+ 'Could not find stylesheet "file.css": directory is missing'
14
+ )
15
+ end
16
+ end
17
+ 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
+ expect(doc.html).to eq("<html></html>")
12
+ end
13
+
14
+ it "has an accessor for URL options" do
15
+ document.url_options = {host: "foo.bar"}
16
+ expect(document.url_options).to eq({host: "foo.bar"})
17
+ end
18
+
19
+ it "has a ProviderList" do
20
+ expect(document.asset_providers).to be_instance_of(ProviderList)
21
+ end
22
+
23
+ it "defaults to having just a FilesystemProvider in the provider list" do
24
+ expect(document).to have(1).asset_providers
25
+ provider = document.asset_providers.first
26
+ expect(provider).to 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
+ expect(document.asset_providers).to be_instance_of(ProviderList)
35
+ expect(document.asset_providers.each.to_a).to eq([other_provider])
36
+
37
+ document.asset_providers = old_list
38
+ expect(document.asset_providers).to eq(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
+ expect(document.before_transformation).to eq(callable)
48
+ expect(document.after_transformation).to eq(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
+ expect(before).to receive(:call).with(instance_of(Nokogiri::HTML::Document)).ordered
60
+ expect(Inliner).to receive(:new).ordered.and_return double.as_null_object
61
+ expect(after).to 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
+ expect(result).to have_selector('html > head > title')
86
+ expect(result.at_css('title').text).to eq("Greetings")
87
+
88
+ expect(result).to have_selector('html > body > p')
89
+ paragraph = result.at_css('p')
90
+ expect(paragraph.text).to eq("Hello, world!")
91
+ expect(paragraph.to_xml).to eq('<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
+ expect(result).to 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,69 @@
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
+ expect(FilesystemProvider.new("/tmp").path).to eq("/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
+ expect(FilesystemProvider.new.path).to eq(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
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
32
25
 
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
26
+ stylesheet = provider.find_stylesheet("stylesheets/green.css")
27
+ expect(stylesheet).not_to be_nil
28
+ expect(stylesheet.name).to eq(full_path)
29
+ expect(stylesheet.to_s).to eq(Stylesheet.new("", file_contents).to_s)
47
30
  end
48
31
 
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'
32
+ it "returns nil on non-existant files" do
33
+ expect(provider.find_stylesheet("non/existant.css")).to be_nil
52
34
  end
53
35
 
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'
36
+ it "finds files inside the base path when using absolute paths" do
37
+ full_path = File.join(fixtures_path, "stylesheets", "green.css")
38
+ expect(provider.find_stylesheet("/stylesheets/green.css").name).to eq(full_path)
58
39
  end
59
40
 
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"
41
+ it "does not read files above the base directory" do
42
+ expect {
43
+ provider.find_stylesheet("../#{File.basename(__FILE__)}")
44
+ }.to raise_error FilesystemProvider::InsecurePathError
72
45
  end
46
+ end
73
47
 
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
48
+ describe "finding stylesheets with query strings" do
49
+ it "ignores the query string" do
50
+ full_path = File.join(fixtures_path, "stylesheets", "green.css")
51
+ file_contents = File.read full_path
81
52
 
82
- it "works with double slashes in the path" do
83
- create_file('path/to/foo.css')
84
- provider.find('path/to//foo.css')
53
+ stylesheet = provider.find_stylesheet("/stylesheets/green.css?time=111")
54
+ expect(stylesheet).not_to be_nil
55
+ expect(stylesheet.name).to eq(full_path)
56
+ expect(stylesheet.to_s).to eq(Stylesheet.new("", file_contents).to_s)
85
57
  end
86
58
 
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/)
59
+ it "shows that the query string is ignored inside raised errors" do
60
+ begin
61
+ provider.find_stylesheet!("/foo.css?query-string")
62
+ fail "No error was raised"
63
+ rescue CssNotFound => error
64
+ expect(error.css_name).to eq("foo.css")
65
+ expect(error.to_s).to include("/foo.css?query-string")
66
+ end
91
67
  end
92
68
  end
93
69
  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
+ expect(rendering('<p></p>')).to 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
+ expect(rendering('<p></p>')).to 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
+ expect(rendering('<p></p>')).to 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
+ expect(rendering('<h1></h1>')).to 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
+ expect(rendering('<p class="tip"></p>')).to 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
+ expect(rendering('<p class="safe"></p>')).to 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
+ expect(rendering('<p class="important"></p>')).to 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
+ expect(rendering('<p class="important" id="important"></p>')).to 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
+ expect(document).to have_styling('color' => 'green').at_selector('p')
66
+ expect(document).to 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
+ expect(rendering('<a class="hard-to-spot"></a>')).to 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
+ expect(rendering('<p style="font-size: 1.1em"></p>')).to 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
+ expect(rendering('<p style="color: green"></p>')).to 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
+ expect(rendering('<p class="active"></p>')).to 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
+ expect(rendering('<p class="some-element"></p>')).to 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
+ expect(inliner).to 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
+ expect(result).to have_styling([['color', 'red']]).at_selector('p:first')
134
+ expect(result).to 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