roadie 2.4.3 → 3.0.0.pre1

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