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
data/Rakefile CHANGED
@@ -1,29 +1,12 @@
1
1
  # encoding: utf-8
2
2
  require 'bundler/setup'
3
- require 'appraisal'
4
3
 
5
4
  Bundler::GemHelper.install_tasks
6
5
 
7
- begin
8
- require 'rspec'
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
13
- end
14
-
15
- require 'rspec/core/rake_task'
16
-
17
6
  desc "Run specs"
18
- RSpec::Core::RakeTask.new('spec') do |t|
19
- t.pattern = 'spec/**/*_spec.rb'
20
- t.rspec_opts = ["-c"]
7
+ task :spec do
8
+ sh "bundle exec rspec -f progress"
21
9
  end
22
10
 
23
11
  desc "Default: Run specs"
24
12
  task :default => :spec
25
-
26
- namespace :spec do
27
- desc 'Run specs against all supported versions of Rails'
28
- task :all => ["appraisal:install", "appraisal", "spec"]
29
- end
data/lib/roadie.rb CHANGED
@@ -1,79 +1,24 @@
1
1
  module Roadie
2
- class << self
3
- # Shortcut for inlining CSS using {Inliner}
4
- # @see Inliner
5
- def inline_css(*args)
6
- Roadie::Inliner.new(*args).execute
7
- end
8
-
9
- # Shortcut to Rails.application
10
- def app
11
- Rails.application
12
- end
13
-
14
- # Returns all available providers
15
- def providers
16
- [AssetPipelineProvider, FilesystemProvider]
17
- end
18
-
19
- # Returns the value of +config.roadie.enabled+.
20
- #
21
- # Roadie will disable all processing if this config is set to +false+. If
22
- # you just want to disable CSS inlining without disabling the rest of
23
- # Roadie, pass +css: nil+ to the +defaults+ method inside your mailers.
24
- def enabled?
25
- config.roadie.enabled
26
- end
27
-
28
- # Returns the active provider
29
- #
30
- # If no provider has been configured a new provider will be instantiated
31
- # depending on if the asset pipeline is enabled or not.
32
- #
33
- # If +config.assets.enabled+ is +true+, the {AssetPipelineProvider} will be used
34
- # while {FilesystemProvider} will be used if it is set to +false+.
35
- #
36
- # @see AssetPipelineProvider
37
- # @see FilesystemProvider
38
- def current_provider
39
- return config.roadie.provider if config.roadie.provider
40
-
41
- if assets_enabled?
42
- AssetPipelineProvider.new
43
- else
44
- FilesystemProvider.new
45
- end
46
- end
47
-
48
- # Returns the value of +config.roadie.after_inlining+
49
- #
50
- def after_inlining_handler
51
- config.roadie.after_inlining
52
- end
53
-
54
- private
55
- def config
56
- Roadie.app.config
57
- end
58
-
59
- def assets_enabled?
60
- # In Rails 4.0, config.assets.enabled is nil by default, so we need to
61
- # explicitly make sure it's not false rather than checking for a
62
- # truthy value.
63
- config.respond_to?(:assets) and config.assets and config.assets.enabled != false
64
- end
65
- end
66
2
  end
67
3
 
68
4
  require 'roadie/version'
69
- require 'roadie/css_file_not_found'
5
+ require 'roadie/errors'
6
+
7
+ require 'roadie/stylesheet'
70
8
  require 'roadie/selector'
71
- require 'roadie/style_declaration'
9
+ require 'roadie/style_property'
10
+ require 'roadie/style_properties'
11
+ require 'roadie/style_block'
72
12
 
73
13
  require 'roadie/asset_provider'
74
- require 'roadie/asset_pipeline_provider'
14
+ require 'roadie/provider_list'
75
15
  require 'roadie/filesystem_provider'
16
+ require 'roadie/null_provider'
76
17
 
18
+ require 'roadie/asset_scanner'
19
+ require 'roadie/markup_improver'
20
+ require 'roadie/url_generator'
21
+ require 'roadie/url_rewriter'
22
+ require 'roadie/null_url_rewriter'
77
23
  require 'roadie/inliner'
78
-
79
- require 'roadie/railtie' if defined?(Rails)
24
+ require 'roadie/document'
@@ -1,62 +1,11 @@
1
1
  module Roadie
2
- # @abstract Subclass to create your own providers
3
- class AssetProvider
4
- # The prefix is whatever is prepended to your stylesheets when referenced inside markup.
5
- #
6
- # The prefix is stripped away from any URLs before they are looked up in {#find}:
7
- # find("/assets/posts/comment.css")
8
- # # Same as: (if prefix == "/assets"
9
- # find("posts/comment.css")
10
- attr_reader :prefix
11
-
12
- # @param [String] prefix Prefix of assets as seen from the browser
13
- # @see #prefix
14
- def initialize(prefix = "/assets")
15
- @prefix = prefix
16
- @quoted_prefix = prepare_prefix(prefix)
2
+ # This module can be included in your own code to help you implement the
3
+ # standard behavior for asset providers.
4
+ #
5
+ # It helps you by declaring {#find_stylesheet!} in the terms of #find_stylesheet in your own class.
6
+ module AssetProvider
7
+ def find_stylesheet!(name)
8
+ find_stylesheet(name) or raise CssNotFound, name
17
9
  end
18
-
19
- # Iterates all the passed elements and calls {#find} on them, joining the results with a newline.
20
- #
21
- # @example
22
- # MyProvider.all("first", "second.css", :third)
23
- #
24
- # @param [Array] files The target files to be loaded together
25
- # @raise [CSSFileNotFound] In case any of the elements is not found
26
- # @see #find
27
- def all(files)
28
- files.map { |file| find(file) }.join("\n")
29
- end
30
-
31
- # @abstract Implement in your own subclass
32
- #
33
- # Return the CSS contents of the file specified. A provider should not care about
34
- # the +.css+ extension; it can, however, behave differently if it's passed or not.
35
- #
36
- # If the asset cannot be found, the method should raise {CSSFileNotFound}.
37
- #
38
- # @example
39
- # MyProvider.find("mystyle")
40
- # MyProvider.find("mystyle.css")
41
- # MyProvider.find(:mystyle)
42
- #
43
- # @param [String] name Name of the file requested
44
- # @raise [CSSFileNotFound] In case any of the elements is not found
45
- def find(name)
46
- raise "Not implemented"
47
- end
48
-
49
- private
50
- def prepare_prefix(prefix)
51
- if prefix =~ /^\//
52
- "/?#{Regexp.quote(prefix[1, prefix.size])}"
53
- else
54
- Regexp.quote(prefix)
55
- end
56
- end
57
-
58
- def remove_prefix(name)
59
- name.sub(/^#{@quoted_prefix}\/?/, '').sub(%r{^/}, '').gsub(%r{//+}, '/')
60
- end
61
10
  end
62
11
  end
@@ -0,0 +1,92 @@
1
+ module Roadie
2
+ # @api private
3
+ #
4
+ # The asset scanner's main usage is finding and/or extracting styles from a
5
+ # DOM tree. Referenced styles will be found using the provided asset
6
+ # provider.
7
+ #
8
+ # Any style declaration tagged with +data-roadie-ignore+ will be ignored.
9
+ class AssetScanner
10
+ attr_reader :dom, :asset_provider
11
+
12
+ # @param [Nokogiri::HTML::Document] dom
13
+ # @param [#find_stylesheet!] asset_provider
14
+ def initialize(dom, asset_provider)
15
+ @dom = dom
16
+ @asset_provider = asset_provider
17
+ end
18
+
19
+ # Looks for all non-ignored stylesheets and returns them.
20
+ #
21
+ # This method will *not* mutate the DOM and is safe to call multiple times.
22
+ #
23
+ # The order of the array corresponds with the document order in the DOM.
24
+ #
25
+ # @see #extract_css
26
+ # @return [Enumerable<Stylesheet>] every found stylesheet
27
+ def find_css
28
+ @dom.css(STYLE_ELEMENT_QUERY).map { |element| read_stylesheet(element) }.compact
29
+ end
30
+
31
+ # Looks for all non-ignored stylesheets, removes their references from the
32
+ # DOM and then returns them.
33
+ #
34
+ # This will mutate the DOM tree.
35
+ #
36
+ # The order of the array corresponds with the document order in the DOM.
37
+ #
38
+ # @see #find_css
39
+ # @return [Enumerable<Stylesheet>] every extracted stylesheet
40
+ def extract_css
41
+ @dom.css(STYLE_ELEMENT_QUERY).map { |element|
42
+ stylesheet = read_stylesheet(element)
43
+ element.remove if stylesheet
44
+ stylesheet
45
+ }.compact
46
+ end
47
+
48
+ private
49
+
50
+ STYLE_ELEMENT_QUERY = (
51
+ "style:not([data-roadie-ignore]), " +
52
+ # TODO: When using Nokogiri 1.6.1 and later; we may use a double :not here
53
+ # instead of the extra code inside #read_stylesheet, and the #compact
54
+ # call in #find_css.
55
+ "link[rel=stylesheet][href]:not([data-roadie-ignore])"
56
+ ).freeze
57
+
58
+ # Cleans out stupid CDATA and/or HTML comments from the style text
59
+ # TinyMCE causes this, allegedly
60
+ CLEANING_MATCHER = /
61
+ (^\s* # Beginning-of-lines matches
62
+ (<!\[CDATA\[)|
63
+ (<!--+)
64
+ )|( # End-of-line matches
65
+ (--+>)|
66
+ (\]\]>)
67
+ $)
68
+ /x.freeze
69
+
70
+ def read_stylesheet(element)
71
+ if element.name == "style"
72
+ read_style_element element
73
+ else
74
+ read_link_element element
75
+ end
76
+ end
77
+
78
+ def read_style_element(element)
79
+ Stylesheet.new "(inline)", clean_css(element.text.strip)
80
+ end
81
+
82
+ def read_link_element(element)
83
+ if element['media'] != "print" && element["href"]
84
+ asset_provider.find_stylesheet! element['href']
85
+ end
86
+ end
87
+
88
+ def clean_css(css)
89
+ css.gsub(CLEANING_MATCHER, '')
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,103 @@
1
+ module Roadie
2
+ # The main entry point for Roadie. A document represents a working unit and
3
+ # is built with the input HTML and the configuration options you need.
4
+ #
5
+ # A Document must never be used from two threads at the same time. Reusing
6
+ # Documents is discouraged.
7
+ #
8
+ # Stylesheets are added to the HTML from three different sources:
9
+ # 1. Stylesheets inside the document ( +<style>+ elements)
10
+ # 2. Stylesheets referenced by the DOM ( +<link>+ elements)
11
+ # 3. The internal stylesheet (see {#add_css})
12
+ #
13
+ # The internal stylesheet is used last and gets the highest priority. The
14
+ # rest is used in the same order as browsers are supposed to use them.
15
+ #
16
+ # @attr [#call] before_transformation Callback to call just before {#transform}ation is begun. Will be called with the parsed DOM tree.
17
+ # @attr [#call] after_transformation Callback to call just before {#transform}ation is completed. Will be called with the current DOM tree.
18
+ class Document
19
+ attr_reader :html, :asset_providers
20
+
21
+ # URL options. If none are given no URL rewriting will take place.
22
+ # @see UrlGenerator#initialize
23
+ attr_accessor :url_options
24
+
25
+ attr_accessor :before_transformation, :after_transformation
26
+
27
+ # @param [String] html the input HTML
28
+ def initialize(html)
29
+ @html = html
30
+ @asset_providers = ProviderList.wrap(FilesystemProvider.new)
31
+ @css = ""
32
+ end
33
+
34
+ # Append additional CSS to the document's internal stylesheet.
35
+ # @param [String] new_css
36
+ def add_css(new_css)
37
+ @css << "\n\n" << new_css
38
+ end
39
+
40
+ # Transform the input HTML and returns the processed HTML.
41
+ #
42
+ # Before the transformation begins, the {#before_transformation} callback will be
43
+ # called with the parsed HTML tree, and after all work is complete the
44
+ # {#after_transformation} callback will be invoked.
45
+ #
46
+ # Most of the work is delegated to other classes. A list of them can be seen below.
47
+ #
48
+ # @see MarkupImprover MarkupImprover (improves the markup of the DOM)
49
+ # @see Inliner Inliner (inlines the stylesheets)
50
+ # @see UrlRewriter UrlRewriter (rewrites URLs and makes them absolute)
51
+ #
52
+ # @return [String] the transformed HTML
53
+ def transform
54
+ dom = Nokogiri::HTML.parse html
55
+
56
+ callback before_transformation, dom
57
+
58
+ improve dom
59
+ inline dom
60
+ rewrite_urls dom
61
+
62
+ callback after_transformation, dom
63
+
64
+ # #dup is called since it fixed a few segfaults in certain versions of Nokogiri
65
+ dom.dup.to_html
66
+ end
67
+
68
+ # Assign new asset providers. The supplied list will be wrapped in a {ProviderList} using {ProviderList.wrap}.
69
+ def asset_providers=(list)
70
+ @asset_providers = ProviderList.wrap(list)
71
+ end
72
+
73
+ private
74
+ def stylesheet
75
+ Stylesheet.new "(Document styles)", @css
76
+ end
77
+
78
+ def improve(dom)
79
+ MarkupImprover.new(dom, html).improve
80
+ end
81
+
82
+ def inline(dom)
83
+ dom_stylesheets = AssetScanner.new(dom, asset_providers).extract_css
84
+ Inliner.new(dom_stylesheets + [stylesheet]).inline(dom)
85
+ end
86
+
87
+ def rewrite_urls(dom)
88
+ make_url_rewriter.transform_dom(dom)
89
+ end
90
+
91
+ def make_url_rewriter
92
+ if url_options
93
+ UrlRewriter.new(UrlGenerator.new(url_options))
94
+ else
95
+ NullUrlRewriter.new
96
+ end
97
+ end
98
+
99
+ def callback(callable, dom)
100
+ callable.(dom) if callable.respond_to?(:call)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,57 @@
1
+ module Roadie
2
+ # Base class for all Roadie errors. Rescue this if you want to catch errors
3
+ # from Roadie.
4
+ #
5
+ # If Roadie raises an error that does not inherit this class, please report
6
+ # it as a bug.
7
+ class Error < RuntimeError; end
8
+
9
+ # Raised when a declaration which cannot be parsed is encountered.
10
+ #
11
+ # A declaration is something like "font-size: 12pt;".
12
+ class UnparseableDeclaration < Error; end
13
+
14
+ # Raised when Roadie encounters an invalid URL which cannot be parsed by
15
+ # Ruby's +URI+ class.
16
+ #
17
+ # This could be a hint that something in your HTML or CSS is broken.
18
+ class InvalidUrlPath < Error
19
+ # The original error, raised from +URI+.
20
+ attr_reader :cause
21
+
22
+ def initialize(given_path, cause = nil)
23
+ @cause = cause
24
+ if cause
25
+ cause_message = " Caused by: #{cause}"
26
+ else
27
+ cause_message = ""
28
+ end
29
+ super "Cannot use path \"#{given_path}\" in URL generation.#{cause_message}"
30
+ end
31
+ end
32
+
33
+ # Raised when an asset provider cannot find a stylesheet.
34
+ #
35
+ # If you are writing your own asset provider, make sure to raise this in the
36
+ # +#find_stylesheet!+ method.
37
+ #
38
+ # @see AssetProvider
39
+ class CssNotFound < Error
40
+ # The name of the stylesheet that cannot be found
41
+ attr_reader :css_name
42
+
43
+ def initialize(css_name, extra_message = nil)
44
+ @css_name = css_name
45
+ super build_message(extra_message)
46
+ end
47
+
48
+ private
49
+ def build_message(extra_message)
50
+ if extra_message
51
+ %(Could not find stylesheet "#{css_name}": #{extra_message})
52
+ else
53
+ %(Could not find stylesheet "#{css_name}")
54
+ end
55
+ end
56
+ end
57
+ end