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,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