roadie 2.4.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,29 @@
1
+ require 'spec_helper'
2
+ require 'roadie/rspec'
3
+
4
+ describe TestProvider do
5
+ subject(:provider) { TestProvider.new }
6
+
7
+ it_behaves_like "roadie asset provider", valid_name: "existing.css", invalid_name: "invalid.css" do
8
+ subject { TestProvider.new "existing.css" => "" }
9
+ end
10
+
11
+ it "finds styles from a predefined hash" do
12
+ provider = TestProvider.new({
13
+ "foo.css" => "a { color: red; }",
14
+ "bar.css" => "body { color: green; }",
15
+ })
16
+ expect(provider.find_stylesheet("foo.css").to_s).not_to include("body")
17
+ expect(provider.find_stylesheet("bar.css").to_s).to include("body")
18
+ expect(provider.find_stylesheet("baz.css")).to be_nil
19
+ end
20
+
21
+ it "can have a default for missing entries" do
22
+ provider = TestProvider.new({
23
+ "foo.css" => "a { color: red; }",
24
+ :default => "body { color: green; }",
25
+ })
26
+ expect(provider.find_stylesheet("foo.css").to_s).not_to include("body")
27
+ expect(provider.find_stylesheet("bar.css").to_s).to include("body")
28
+ end
29
+ end
@@ -0,0 +1,121 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+
4
+ module Roadie
5
+ describe UrlGenerator do
6
+ it "is initialized with URL options" do
7
+ expect(UrlGenerator.new(host: "foo.com").url_options).to eq({host: "foo.com"})
8
+ end
9
+
10
+ it "raises an argument error if no URL options are specified" do
11
+ expect {
12
+ UrlGenerator.new(nil)
13
+ }.to raise_error ArgumentError, /url options/i
14
+ end
15
+
16
+ it "raises an argument error if no host is specified" do
17
+ expect {
18
+ UrlGenerator.new(port: 3000)
19
+ }.to raise_error ArgumentError, /host/i
20
+ end
21
+
22
+ it "raises an argument error if unknown option is passed" do
23
+ expect {
24
+ UrlGenerator.new(host: "localhost", secret: true)
25
+ }.to raise_error ArgumentError, /secret/
26
+ end
27
+
28
+ describe "generating URLs" do
29
+ def url(path, options = {})
30
+ UrlGenerator.new(options).generate_url(path)
31
+ end
32
+
33
+ it "uses the given host" do
34
+ expect(url("/hello.jpg", host: "goats.com")).to eq("http://goats.com/hello.jpg")
35
+ end
36
+
37
+ it "uses the given port" do
38
+ expect(url("/", host: "example.com", port: 1337)).to eq("http://example.com:1337/")
39
+ end
40
+
41
+ it "uses the given scheme" do
42
+ expect(url("/", host: "example.com", scheme: "https")).to eq("https://example.com/")
43
+ end
44
+
45
+ it "regards :protocol as an alias for scheme" do
46
+ expect(url("/", host: "example.com", protocol: "https")).to eq("https://example.com/")
47
+ end
48
+
49
+ it "strips extra characters from the scheme" do
50
+ expect(url("/", host: "example.com", scheme: "https://")).to eq("https://example.com/")
51
+ end
52
+
53
+ it "uses the given path as a prefix" do
54
+ expect(url("/my_file", host: "example.com", path: "/my_app")).to eq(
55
+ "http://example.com/my_app/my_file"
56
+ )
57
+ end
58
+
59
+ it "returns the original URL if it is absolute" do
60
+ expect(url("http://foo.com/", host: "bar.com")).to eq("http://foo.com/")
61
+ end
62
+
63
+ it "returns the base URL for blank paths" do
64
+ expect(url("", host: "foo.com")).to eq("http://foo.com")
65
+ expect(url(nil, host: "foo.com")).to eq("http://foo.com")
66
+ end
67
+
68
+ it "raises an error on invalid path" do
69
+ expect {
70
+ url("://", host: "example.com")
71
+ }.to raise_error InvalidUrlPath, %r{://}
72
+ end
73
+
74
+ it "accepts base paths without a slash in the beginning" do
75
+ expect(url("/bar", host: "example.com", path: "foo")).to eq("http://example.com/foo/bar")
76
+ expect(url("/bar/", host: "example.com", path: "foo/")).to eq("http://example.com/foo/bar/")
77
+ end
78
+
79
+ it "accepts input paths without a slash in the beginning" do
80
+ expect(url("bar", host: "example.com", path: "/foo")).to eq("http://example.com/foo/bar")
81
+ expect(url("bar", host: "example.com", path: "/foo/")).to eq("http://example.com/foo/bar")
82
+ end
83
+
84
+ it "does not touch data: URIs" do
85
+ expect(url("data:deadbeef", host: "example.com")).to eq("data:deadbeef")
86
+ end
87
+ end
88
+
89
+ # URLs in resources that are not based inside the root requires that we may
90
+ # specify a "custom base" to properly handle relative paths. Here's an
91
+ # example:
92
+ #
93
+ # # /
94
+ # # document.html
95
+ # # images/
96
+ # # bg.png
97
+ # # stylesheets/
98
+ # # my_style.css
99
+ #
100
+ # # stylesheets/my_style.css
101
+ # body { background-image: url(../images/bg.png); }
102
+ #
103
+ # In this example, the stylesheet refers to /images/bg.png by using a
104
+ # relative path from /stylesheets/. In order to understand these cases, we
105
+ # need to specify where the "base" is in relation to the root.
106
+ describe "generating URLs with custom base" do
107
+ it "resolves relative paths" do
108
+ generator = UrlGenerator.new(host: "foo.com")
109
+ expect(generator.generate_url("../images/bg.png", "/stylesheets")).to eq("http://foo.com/images/bg.png")
110
+ expect(generator.generate_url("../images/bg.png", "email/stylesheets")).to eq("http://foo.com/email/images/bg.png")
111
+ expect(generator.generate_url("images/bg.png", "email/")).to eq("http://foo.com/email/images/bg.png")
112
+ end
113
+
114
+ it "does not use the base when presented with a root-based path" do
115
+ generator = UrlGenerator.new(host: "foo.com")
116
+ expect(generator.generate_url("/images/bg.png", "/stylesheets")).to eq("http://foo.com/images/bg.png")
117
+ expect(generator.generate_url("/images/bg.png", "email/stylesheets")).to eq("http://foo.com/images/bg.png")
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+ require 'shared_examples/url_rewriter'
3
+
4
+ module Roadie
5
+ describe UrlRewriter do
6
+ let(:generator) { double("URL generator") }
7
+ subject(:rewriter) { UrlRewriter.new(generator) }
8
+
9
+ it_behaves_like "url rewriter"
10
+
11
+ describe "transforming DOM trees" do
12
+ def dom_document(html); Nokogiri::HTML.parse html; end
13
+
14
+ it "rewrites all a[href]" do
15
+ expect(generator).to receive(:generate_url).with("some/path").and_return "http://foo.com/"
16
+ dom = dom_document <<-HTML
17
+ <body>
18
+ <a href="some/path">Some path</a>
19
+ </body>
20
+ HTML
21
+
22
+ expect {
23
+ rewriter.transform_dom dom
24
+ }.to change { dom.at_css("a")["href"] }.to "http://foo.com/"
25
+ end
26
+
27
+ it "rewrites relative img[src]" do
28
+ expect(generator).to receive(:generate_url).with("some/path.jpg").and_return "http://foo.com/image.jpg"
29
+ dom = dom_document <<-HTML
30
+ <body>
31
+ <img src="some/path.jpg">
32
+ </body>
33
+ HTML
34
+
35
+ expect {
36
+ rewriter.transform_dom dom
37
+ }.to change { dom.at_css("img")["src"] }.to "http://foo.com/image.jpg"
38
+ end
39
+
40
+ it "rewrites url() directives inside style attributes" do
41
+ expect(generator).to receive(:generate_url).with("some/path.jpg").and_return "http://foo.com/image.jpg"
42
+ dom = dom_document <<-HTML
43
+ <body>
44
+ <div style="background-image: url(&quot;some/path.jpg&quot;);">
45
+ </body>
46
+ HTML
47
+
48
+ expect {
49
+ rewriter.transform_dom dom
50
+ }.to change { dom.at_css("div")["style"] }.to 'background-image: url("http://foo.com/image.jpg");'
51
+ end
52
+ end
53
+
54
+ describe "transforming css" do
55
+ it "rewrites all url() directives" do
56
+ expect(generator).to receive(:generate_url).with("some/path.jpg").and_return "http://foo.com/image.jpg"
57
+ css = "body { background: top url(some/path.jpg) #eee; }"
58
+ expect {
59
+ rewriter.transform_css css
60
+ }.to change { css }.to "body { background: top url(http://foo.com/image.jpg) #eee; }"
61
+ end
62
+
63
+ it "correctly identifies URLs with single quotes" do
64
+ expect(generator).to receive(:generate_url).with("images/foo.png").and_return "x"
65
+ rewriter.transform_css "url('images/foo.png')"
66
+ end
67
+
68
+ it "correctly identifies URLs with double quotes" do
69
+ expect(generator).to receive(:generate_url).with("images/foo.png").and_return "x"
70
+ rewriter.transform_css 'url("images/foo.png")'
71
+ end
72
+
73
+ it "correctly identifies URLs with parenthesis inside them" do
74
+ expect(generator).to receive(:generate_url).with("images/map_(large_(extra)).png").and_return "x"
75
+ rewriter.transform_css 'url(images/map_(large_(extra)).png)'
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,11 @@
1
+ shared_examples_for "asset provider role" do
2
+ it "responds to #find_stylesheet" do
3
+ expect(subject).to respond_to(:find_stylesheet)
4
+ expect(subject.method(:find_stylesheet).arity).to eq(1)
5
+ end
6
+
7
+ it "responds to #find_stylesheet!" do
8
+ expect(subject).to respond_to(:find_stylesheet!)
9
+ expect(subject.method(:find_stylesheet!).arity).to eq(1)
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ shared_examples_for "url rewriter" do
2
+ it "is constructed with a generator" do
3
+ generator = double "URL generator"
4
+ expect {
5
+ described_class.new(generator)
6
+ }.to_not raise_error
7
+ end
8
+
9
+ it "has a #transform_dom(dom) method that returns nil" do
10
+ expect(subject).to respond_to(:transform_dom)
11
+ expect(subject.method(:transform_dom).arity).to eq(1)
12
+
13
+ dom = Nokogiri::HTML.parse "<body></body>"
14
+ expect(subject.transform_dom(dom)).to be_nil
15
+ end
16
+
17
+ it "has a #transform_css(css) method that returns nil" do
18
+ expect(subject).to respond_to(:transform_css)
19
+ expect(subject.method(:transform_css).arity).to eq(1)
20
+
21
+ expect(subject.transform_css("")).to be_nil
22
+ end
23
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,68 +1,14 @@
1
- $: << File.dirname(__FILE__) + '/../lib'
2
-
3
- require 'ostruct'
4
- require 'rubygems'
5
- require 'bundler'
1
+ require 'rspec/collection_matchers'
6
2
 
7
- begin
8
- Bundler.setup(:default, :development)
9
- rescue Bundler::BundlerError => e
10
- $stderr.puts e.message
11
- $stderr.puts "Run `bundle install` to install missing gems"
12
- exit e.status_code
3
+ if ENV['CI']
4
+ require 'coveralls'
5
+ Coveralls.wear!
13
6
  end
14
7
 
15
- require 'rspec'
16
- require 'rails'
17
- require 'sprockets'
18
-
19
- require 'roadie/railtie'
20
- require 'action_mailer/railtie'
21
-
22
- FIXTURES_PATH = Pathname.new(File.dirname(__FILE__)).join('fixtures')
23
-
24
- class TestApplication < Rails::Application
25
- def config
26
- @config
27
- end
28
-
29
- def assets
30
- env = Sprockets::Environment.new
31
- env.append_path root.join('app','assets','stylesheets')
32
- env
33
- end
34
-
35
- def root
36
- FIXTURES_PATH
37
- end
38
-
39
- def reset_test_config
40
- @config = OpenStruct.new({
41
- :action_mailer => OpenStruct.new(:default_url_options => {}),
42
- :assets => OpenStruct.new(:enabled => false),
43
- :roadie => OpenStruct.new(:provider => nil, :enabled => true),
44
- })
45
- change_default_url_options(:host => "example.com")
46
- end
47
- end
48
-
49
- if Roadie::Railtie.respond_to?(:run_initializers)
50
- # Rails >= 3.1
51
- ActionMailer::Railtie.run_initializers(:default, Rails.application)
52
- Roadie::Railtie.run_initializers(:default, Rails.application)
53
- else
54
- # Rails 3.0
55
- Rails.application.config.active_support.deprecation = :log
56
- Rails.logger = Logger.new('/dev/null')
57
- Rails.application.initialize!
58
- end
8
+ $: << File.dirname(__FILE__) + '/../lib'
9
+ require 'roadie'
59
10
 
60
11
  RSpec.configure do |config|
61
- config.before(:each) do
62
- Rails.application.reset_test_config
63
- end
64
-
65
- config.treat_symbols_as_metadata_keys_with_true_values = true
66
12
  config.run_all_when_everything_filtered = true
67
13
  end
68
14
 
@@ -11,11 +11,11 @@ RSpec::Matchers.define :have_attribute do |attribute|
11
11
  end
12
12
 
13
13
  description { "have attribute #{attribute.inspect} at selector #{@selector.inspect}" }
14
- failure_message_for_should do |document|
14
+ failure_message do |document|
15
15
  name, expected = attribute.first
16
16
  "expected #{name} attribute at #{@selector.inspect} to be #{expected.inspect} but was #{attribute(document, name).inspect}"
17
17
  end
18
- failure_message_for_should_not do |document|
18
+ failure_message_when_negated do |document|
19
19
  name, expected = attribute.first
20
20
  "expected #{name} attribute at #{@selector.inspect} to not be #{expected.inspect}"
21
21
  end
@@ -3,14 +3,14 @@ RSpec::Matchers.define :have_node do |selector|
3
3
  match do |document|
4
4
  node = document.at_css(selector)
5
5
  if @attributes
6
- node.present? and match_attributes(node.attributes)
6
+ node && match_attributes(node.attributes)
7
7
  else
8
- node.present?
8
+ node
9
9
  end
10
10
  end
11
11
 
12
- failure_message_for_should { "expected document to #{name_to_sentence}#{expected_to_sentence}"}
13
- failure_message_for_should_not { "expected document to not #{name_to_sentence}#{expected_to_sentence}"}
12
+ failure_message { "expected document to #{name_to_sentence}#{expected_to_sentence}"}
13
+ failure_message_when_negated { "expected document to not #{name_to_sentence}#{expected_to_sentence}"}
14
14
 
15
15
  def match_attributes(node_attributes)
16
16
  attributes = Hash[node_attributes.map { |name, attribute| [name, attribute.value] }]
@@ -1,6 +1,6 @@
1
1
  RSpec::Matchers.define :have_selector do |selector|
2
- match { |document| document.css(selector).present? }
3
- failure_message_for_should { "expected document to #{name_to_sentence}#{expected_to_sentence}"}
4
- failure_message_for_should_not { "expected document to not #{name_to_sentence}#{expected_to_sentence}"}
2
+ match { |document| !document.css(selector).empty? }
3
+ failure_message { "expected document to #{name_to_sentence}#{to_sentence selector}"}
4
+ failure_message_when_negated { "expected document to not #{name_to_sentence}#{to_sentence selector}"}
5
5
  end
6
6
 
@@ -1,25 +1,61 @@
1
1
  RSpec::Matchers.define :have_styling do |rules|
2
- @selector = 'body > *:first'
2
+ normalized_rules = StylingExpectation.new(rules)
3
3
 
4
- chain :at_selector do |selector|
5
- @selector = selector
4
+ chain(:at_selector) { |selector| @selector = selector }
5
+ match { |document|
6
+ @selector ||= 'body > *:first'
7
+ normalized_rules == styles_at_selector(document)
8
+ }
9
+
10
+ description {
11
+ "have styles #{normalized_rules.inspect} at selector #{@selector.inspect}"
12
+ }
13
+
14
+ failure_message { |document|
15
+ "expected styles at #{@selector.inspect} to be:\n#{normalized_rules}\nbut was:\n#{styles_at_selector(document)}"
16
+ }
17
+
18
+ failure_message_when_negated {
19
+ "expected styles at #{@selector.inspect} to not be:\n#{normalized_rules}"
20
+ }
21
+
22
+ def styles_at_selector(document)
23
+ expect(document).to have_selector(@selector)
24
+ StylingExpectation.new document.at_css(@selector)['style']
6
25
  end
26
+ end
7
27
 
8
- match do |document|
9
- if rules.nil?
10
- parsed_styles(document).blank?
11
- else
12
- rules.to_a.should == parsed_styles(document)
28
+ class StylingExpectation
29
+ def initialize(styling)
30
+ case styling
31
+ when String then @rules = parse_rules(styling)
32
+ when Array then @rules = styling
33
+ when Hash then @rules = styling.to_a
34
+ else fail "I don't understand #{styling.inspect}!"
13
35
  end
14
36
  end
15
37
 
16
- description { "have styles #{rules.inspect} at selector #{@selector.inspect}" }
17
- failure_message_for_should { |document| "expected styles at #{@selector.inspect} to be #{rules.inspect} but was #{parsed_styles(document).inspect}" }
18
- failure_message_for_should_not { "expected styles at #{@selector.inspect} to not be #{rules.inspect}" }
38
+ def ==(other)
39
+ rules == other.rules
40
+ end
41
+
42
+ def to_s() rules.to_s end
19
43
 
20
- def parsed_styles(document)
21
- node = document.css(@selector).first
22
- SpecHelpers.styling_of_node(node)
44
+ protected
45
+ attr_reader :rules
46
+
47
+ private
48
+ def parse_rules(css)
49
+ css.split(';').map { |property| parse_property(property) }
50
+ end
51
+
52
+ def parse_property(property)
53
+ rule, value = property.split(':', 2).map(&:strip)
54
+ [rule, normalize_quotes(value)]
23
55
  end
24
- end
25
56
 
57
+ # JRuby's Nokogiri encodes quotes
58
+ def normalize_quotes(string)
59
+ string.gsub '%22', '"'
60
+ end
61
+ end