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,13 @@
1
+ module Roadie
2
+ # An asset provider that returns empty stylesheets for any name.
3
+ #
4
+ # Use it to ignore missing assets or in your tests when you need a provider
5
+ # but you do not care what it contains or that it is even referenced at all.
6
+ class NullProvider
7
+ def find_stylesheet(name) empty_stylesheet end
8
+ def find_stylesheet!(name) empty_stylesheet end
9
+
10
+ private
11
+ def empty_stylesheet() Stylesheet.new "(null)", "" end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Roadie
2
+ # @api private
3
+ # Null Object for the URL rewriter role.
4
+ #
5
+ # Used whenever client does not pass any URL options and no URL rewriting
6
+ # should take place.
7
+ class NullUrlRewriter
8
+ def initialize(generator = nil) end
9
+ def transform_dom(dom) end
10
+ def transform_css(css) end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ require 'forwardable'
2
+
3
+ module Roadie
4
+ # An asset provider that just composes a list of other asset providers.
5
+ #
6
+ # Give it a list of providers and they will all be tried in order.
7
+ #
8
+ # {ProviderList} behaves like an Array, *and* an asset provider, and can be coerced into an array.
9
+ class ProviderList
10
+ extend Forwardable
11
+ include Enumerable
12
+ include AssetProvider
13
+
14
+ # Wrap a single provider, or a list of providers into a {ProviderList}.
15
+ #
16
+ # @overload wrap(provider_list)
17
+ # @param [ProviderList] provider_list An actual instance of {ProviderList}.
18
+ # @return The passed in provider_list
19
+ #
20
+ # @overload wrap(provider)
21
+ # @param [asset provider] provider
22
+ # @return a new {ProviderList} with just the passed provider in it
23
+ #
24
+ # @overload wrap(provider1, provider2, ...)
25
+ # @return a new {ProviderList} with all the passed providers in it.
26
+ def self.wrap(*providers)
27
+ if providers.size == 1 && providers.first.class == self
28
+ providers.first
29
+ else
30
+ new(providers.flatten)
31
+ end
32
+ end
33
+
34
+ def initialize(providers)
35
+ @providers = providers
36
+ end
37
+
38
+ # @return [Stylesheet, nil]
39
+ def find_stylesheet(name)
40
+ @providers.each do |provider|
41
+ css = provider.find_stylesheet(name)
42
+ return css if css
43
+ end
44
+ nil
45
+ end
46
+
47
+ # ProviderList can be coerced to an array. This makes Array#flatten work
48
+ # with it, among other things.
49
+ def to_ary() to_a end
50
+
51
+ # @!method each
52
+ # @see Array#each
53
+ # @!method size
54
+ # @see Array#size
55
+ # @!method push
56
+ # @see Array#push
57
+ # @!method <<
58
+ # @see Array#<<
59
+ # @!method pop
60
+ # @see Array#pop
61
+ # @!method unshift
62
+ # @see Array#unshift
63
+ # @!method shift
64
+ # @see Array#shift
65
+ def_delegators :@providers, :each, :size, :push, :<<, :pop, :unshift, :shift
66
+ end
67
+ end
@@ -0,0 +1 @@
1
+ require 'roadie/rspec/asset_provider'
@@ -0,0 +1,49 @@
1
+ shared_examples_for "roadie asset provider" do |options|
2
+ valid_name = options[:valid_name] or raise "You must provide a :valid_name option to the shared examples"
3
+ invalid_name = options[:invalid_name] or raise "You must provide an :invalid_name option to the shared examples"
4
+
5
+ def verify_stylesheet(stylesheet)
6
+ stylesheet.should_not be_nil
7
+
8
+ # Name
9
+ stylesheet.name.should be_a(String)
10
+ stylesheet.name.should_not be_empty
11
+
12
+ # We do not want to force clients to always return non-empty files.
13
+ # Stylesheet#initialize should crash when given a non-valid CSS (like nil,
14
+ # for example)
15
+ # stylesheet.blocks.should_not be_empty
16
+ end
17
+
18
+ it "responds to #find_stylesheet" do
19
+ subject.should respond_to(:find_stylesheet)
20
+ subject.method(:find_stylesheet).arity.should == 1
21
+ end
22
+
23
+ it "responds to #find_stylesheet!" do
24
+ subject.should respond_to(:find_stylesheet!)
25
+ subject.method(:find_stylesheet!).arity.should == 1
26
+ end
27
+
28
+ describe "#find_stylesheet" do
29
+ it "can find a stylesheet" do
30
+ verify_stylesheet subject.find_stylesheet(valid_name)
31
+ end
32
+
33
+ it "cannot find an invalid stylesheet" do
34
+ subject.find_stylesheet(invalid_name).should be_nil
35
+ end
36
+ end
37
+
38
+ describe "#find_stylesheet!" do
39
+ it "can find a stylesheet" do
40
+ verify_stylesheet subject.find_stylesheet!(valid_name)
41
+ end
42
+
43
+ it "raises Roadie::CssNotFound on invalid stylesheets" do
44
+ expect {
45
+ subject.find_stylesheet!(invalid_name)
46
+ }.to raise_error Roadie::CssNotFound, Regexp.new(Regexp.quote(invalid_name))
47
+ end
48
+ end
49
+ end
@@ -1,24 +1,46 @@
1
1
  module Roadie
2
+ # @api private
3
+ #
4
+ # A selector is a domain object for a CSS selector, such as:
5
+ # body
6
+ # a:hover
7
+ # input::placeholder
8
+ # p:nth-of-child(4n+1) .important a img
9
+ #
10
+ # "Selectors" such as "strong, em" are actually two selectors and should be
11
+ # represented as two instances of this class.
12
+ #
13
+ # This class can also calculate specificity for the selector and answer a few
14
+ # questions about them.
15
+ #
16
+ # Selectors can be coerced into Strings, so they should be transparent to use
17
+ # anywhere a String is expected.
2
18
  class Selector
3
- def initialize(selector)
19
+ def initialize(selector, specificity = nil)
4
20
  @selector = selector.to_s.strip
21
+ @specificity = specificity
5
22
  end
6
23
 
24
+ # Returns the specificity of the selector, calculating it if needed.
7
25
  def specificity
8
26
  @specificity ||= CssParser.calculate_specificity selector
9
27
  end
10
28
 
29
+ # Returns whenever or not a selector can be inlined.
30
+ # It's impossible to inline properties that applies to a pseudo element
31
+ # (like +::placeholder+, +::before+) or a pseudo function (like +:active+).
32
+ #
33
+ # We cannot inline styles that appear inside "@" constructs, like +@keyframes+.
11
34
  def inlinable?
12
35
  !(pseudo_element? || at_rule? || pseudo_function?)
13
36
  end
14
37
 
15
- def to_s
16
- selector
17
- end
18
-
38
+ def to_s() selector end
19
39
  def to_str() to_s end
20
40
  def inspect() selector.inspect end
21
41
 
42
+ # {Selector}s are equal to other {Selector}s if, and only if, their string
43
+ # representations are equal.
22
44
  def ==(other)
23
45
  if other.is_a?(self.class)
24
46
  other.selector == selector
@@ -28,23 +50,25 @@ module Roadie
28
50
  end
29
51
 
30
52
  protected
31
- attr_reader :selector
53
+ attr_reader :selector
32
54
 
33
55
  private
34
- BAD_PSEUDO_FUNCTIONS = %w[:active :focus :hover :link :target :visited
35
- :-ms-input-placeholder :-moz-placeholder
36
- :before :after :enabled :disabled :checked].freeze
56
+ BAD_PSEUDO_FUNCTIONS = %w[
57
+ :active :focus :hover :link :target :visited
58
+ :-ms-input-placeholder :-moz-placeholder
59
+ :before :after
60
+ ].freeze
37
61
 
38
- def pseudo_element?
39
- selector.include? '::'
40
- end
62
+ def pseudo_element?
63
+ selector.include? '::'
64
+ end
41
65
 
42
- def at_rule?
43
- selector[0, 1] == '@'
44
- end
66
+ def at_rule?
67
+ selector[0, 1] == '@'
68
+ end
45
69
 
46
- def pseudo_function?
47
- BAD_PSEUDO_FUNCTIONS.any? { |bad| selector.include?(bad) }
48
- end
70
+ def pseudo_function?
71
+ BAD_PSEUDO_FUNCTIONS.any? { |bad| selector.include?(bad) }
72
+ end
49
73
  end
50
74
  end
@@ -0,0 +1,33 @@
1
+ require 'forwardable'
2
+
3
+ module Roadie
4
+ # @api private
5
+ # A style block is the combination of a {Selector} and a list of {StyleProperty}.
6
+ class StyleBlock
7
+ extend Forwardable
8
+ attr_reader :selector, :properties
9
+
10
+ # @param [Selector] selector
11
+ # @param [Array<StyleProperty>] properties
12
+ def initialize(selector, properties)
13
+ @selector = selector
14
+ # TODO: Should we use {StyleProperties} instead? Why? Why not?
15
+ @properties = properties
16
+ end
17
+
18
+ # @!method specificity
19
+ # @see Selector#specificity
20
+ # @!method inlinable?
21
+ # @see Selector#inlinable?
22
+ def_delegators :selector, :specificity, :inlinable?
23
+ # @!method selector_string
24
+ # @see Selector#to_s
25
+ def_delegator :selector, :to_s, :selector_string
26
+
27
+ # String representation of the style block. This is valid CSS and can be
28
+ # used in the DOM.
29
+ def to_s
30
+ "#{selector}{#{properties.map(&:to_s).join(';')}}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ module Roadie
2
+ # @api private
3
+ # Stores orphan style properties as they are being merged into specific
4
+ # element's "style" attribute.
5
+ class StyleProperties
6
+ attr_reader :properties
7
+
8
+ def initialize(properties)
9
+ @properties = properties
10
+ end
11
+
12
+ def merge(new_properties)
13
+ StyleProperties.new(properties + properties_of(new_properties))
14
+ end
15
+
16
+ def merge!(new_properties)
17
+ @properties += properties_of(new_properties)
18
+ end
19
+
20
+ def to_s
21
+ @properties.sort.map(&:to_s).join(";")
22
+ end
23
+
24
+ private
25
+ def properties_of(object)
26
+ object.respond_to?(:properties) ? object.properties : object
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,93 @@
1
+ module Roadie
2
+ # @api private
3
+ # Domain object for a CSS property such as "color: red !important".
4
+ #
5
+ # @attr_reader [String] property name of the property (such as "font-size").
6
+ # @attr_reader [String] value value of the property (such as "5px solid green").
7
+ # @attr_reader [Boolean] important if the property is "!important".
8
+ # @attr_reader [Integer] specificity specificity of parent {Selector}. Used to compare/sort.
9
+ class StyleProperty
10
+ include Comparable
11
+
12
+ attr_reader :value, :important, :specificity
13
+
14
+ # @todo Rename #property to #name
15
+ attr_reader :property
16
+
17
+ # Parse a property string.
18
+ #
19
+ # @example
20
+ # property = Roadie::StyleProperty.parse("color: green")
21
+ # property.property # => "color"
22
+ # property.value # => "green"
23
+ # property.important? # => false
24
+ def self.parse(declaration, specificity)
25
+ allocate.send :read_declaration!, declaration, specificity
26
+ end
27
+
28
+ def initialize(property, value, important, specificity)
29
+ @property = property
30
+ @value = value
31
+ @important = important
32
+ @specificity = specificity
33
+ end
34
+
35
+ def important?
36
+ @important
37
+ end
38
+
39
+ # Compare another {StyleProperty}. Important styles are "greater than"
40
+ # non-important ones; otherwise the specificity declares order.
41
+ def <=>(other)
42
+ if important == other.important
43
+ specificity <=> other.specificity
44
+ else
45
+ important ? 1 : -1
46
+ end
47
+ end
48
+
49
+ def to_s
50
+ [property, value_with_important].join(':')
51
+ end
52
+
53
+ def inspect
54
+ "#{to_s} (#{specificity})"
55
+ end
56
+
57
+ protected
58
+ def read_declaration!(declaration, specificity)
59
+ if (matches = DECLARATION_MATCHER.match(declaration))
60
+ initialize matches[:property], matches[:value].strip, !!matches[:important], specificity
61
+ self
62
+ else
63
+ raise UnparseableDeclaration, "Cannot parse declaration #{declaration.inspect}"
64
+ end
65
+ end
66
+
67
+ private
68
+ DECLARATION_MATCHER = %r{
69
+ \A\s*
70
+ (?:
71
+ # !important declaration
72
+ (?<property>[^:]+):\s?
73
+ (?<value>.*?)
74
+ (?<important>\s!important)
75
+ ;?
76
+ |
77
+ # normal declaration
78
+ (?<property>[^:]+):\s?
79
+ (?<value>[^;]+)
80
+ ;?
81
+ )
82
+ \s*\Z
83
+ }x.freeze
84
+
85
+ def value_with_important
86
+ if important
87
+ "#{value} !important"
88
+ else
89
+ value
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,65 @@
1
+ module Roadie
2
+ # Domain object that represents a stylesheet (from disc, perhaps).
3
+ #
4
+ # It has a name and a list of {StyleBlock}s.
5
+ #
6
+ # @attr_reader [String] name the name of the stylesheet ("stylesheets/main.css", "Admin user styles", etc.). The name of the stylesheet will be visible if any errors occur.
7
+ # @attr_reader [Array<StyleBlock>] blocks
8
+ class Stylesheet
9
+ attr_reader :name, :blocks
10
+
11
+ # Parses the CSS string into a {StyleBlock}s and stores it.
12
+ #
13
+ # @param [String] name
14
+ # @param [String] css
15
+ def initialize(name, css)
16
+ @name = name
17
+ @blocks = parse_blocks(css)
18
+ end
19
+
20
+ # @yield [selector, properties]
21
+ # @yieldparam [Selector] selector
22
+ # @yieldparam [Array<StyleProperty>] properties
23
+ def each_inlinable_block(&block)
24
+ # #map and then #each in order to support chained enumerations, etc. if
25
+ # no block is provided
26
+ inlinable_blocks.map { |style_block|
27
+ [style_block.selector, style_block.properties]
28
+ }.each(&block)
29
+ end
30
+
31
+ def to_s
32
+ blocks.join("\n")
33
+ end
34
+
35
+ private
36
+ def inlinable_blocks
37
+ blocks.select(&:inlinable?)
38
+ end
39
+
40
+ def parse_blocks(css)
41
+ blocks = []
42
+ setup_parser(css).each_selector do |selector_string, declarations, specificity|
43
+ blocks << create_style_block(selector_string, declarations, specificity)
44
+ end
45
+ blocks
46
+ end
47
+
48
+ def create_style_block(selector_string, declarations, specificity)
49
+ StyleBlock.new(
50
+ Selector.new(selector_string, specificity),
51
+ parse_declarations(declarations, specificity)
52
+ )
53
+ end
54
+
55
+ def setup_parser(css)
56
+ parser = CssParser::Parser.new
57
+ parser.add_block! css
58
+ parser
59
+ end
60
+
61
+ def parse_declarations(declarations, specificity)
62
+ declarations.split(';').map { |declaration| StyleProperty.parse(declaration, specificity) }
63
+ end
64
+ end
65
+ end