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