md-roadie 2.4.2.md.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.autotest +10 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +22 -0
  4. data/.yardopts +1 -0
  5. data/Appraisals +15 -0
  6. data/Changelog.md +185 -0
  7. data/Gemfile +11 -0
  8. data/Guardfile +8 -0
  9. data/MIT-LICENSE +20 -0
  10. data/README.md +310 -0
  11. data/Rakefile +30 -0
  12. data/autotest/discover.rb +1 -0
  13. data/gemfiles/rails_3.0.gemfile +7 -0
  14. data/gemfiles/rails_3.0.gemfile.lock +123 -0
  15. data/gemfiles/rails_3.1.gemfile +7 -0
  16. data/gemfiles/rails_3.1.gemfile.lock +126 -0
  17. data/gemfiles/rails_3.2.gemfile +7 -0
  18. data/gemfiles/rails_3.2.gemfile.lock +124 -0
  19. data/gemfiles/rails_4.0.gemfile +7 -0
  20. data/gemfiles/rails_4.0.gemfile.lock +119 -0
  21. data/lib/roadie.rb +79 -0
  22. data/lib/roadie/action_mailer_extensions.rb +95 -0
  23. data/lib/roadie/asset_pipeline_provider.rb +28 -0
  24. data/lib/roadie/asset_provider.rb +62 -0
  25. data/lib/roadie/css_file_not_found.rb +22 -0
  26. data/lib/roadie/filesystem_provider.rb +74 -0
  27. data/lib/roadie/inliner.rb +251 -0
  28. data/lib/roadie/railtie.rb +39 -0
  29. data/lib/roadie/selector.rb +50 -0
  30. data/lib/roadie/style_declaration.rb +42 -0
  31. data/lib/roadie/version.rb +3 -0
  32. data/md-roadie.gemspec +36 -0
  33. data/spec/fixtures/app/assets/stylesheets/integration.css +10 -0
  34. data/spec/fixtures/public/stylesheets/integration.css +10 -0
  35. data/spec/fixtures/views/integration_mailer/marketing.html.erb +2 -0
  36. data/spec/fixtures/views/integration_mailer/notification.html.erb +8 -0
  37. data/spec/fixtures/views/integration_mailer/notification.text.erb +6 -0
  38. data/spec/integration_spec.rb +110 -0
  39. data/spec/lib/roadie/action_mailer_extensions_spec.rb +227 -0
  40. data/spec/lib/roadie/asset_pipeline_provider_spec.rb +65 -0
  41. data/spec/lib/roadie/css_file_not_found_spec.rb +29 -0
  42. data/spec/lib/roadie/filesystem_provider_spec.rb +94 -0
  43. data/spec/lib/roadie/inliner_spec.rb +591 -0
  44. data/spec/lib/roadie/selector_spec.rb +55 -0
  45. data/spec/lib/roadie/style_declaration_spec.rb +49 -0
  46. data/spec/lib/roadie_spec.rb +101 -0
  47. data/spec/shared_examples/asset_provider_examples.rb +11 -0
  48. data/spec/spec_helper.rb +69 -0
  49. data/spec/support/anonymous_mailer.rb +21 -0
  50. data/spec/support/change_url_options.rb +5 -0
  51. data/spec/support/have_attribute_matcher.rb +28 -0
  52. data/spec/support/have_node_matcher.rb +19 -0
  53. data/spec/support/have_selector_matcher.rb +6 -0
  54. data/spec/support/have_styling_matcher.rb +25 -0
  55. data/spec/support/parse_styling.rb +25 -0
  56. metadata +318 -0
@@ -0,0 +1,79 @@
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
+ end
67
+
68
+ require 'roadie/version'
69
+ require 'roadie/css_file_not_found'
70
+ require 'roadie/selector'
71
+ require 'roadie/style_declaration'
72
+
73
+ require 'roadie/asset_provider'
74
+ require 'roadie/asset_pipeline_provider'
75
+ require 'roadie/filesystem_provider'
76
+
77
+ require 'roadie/inliner'
78
+
79
+ require 'roadie/railtie' if defined?(Rails)
@@ -0,0 +1,95 @@
1
+ require 'uri'
2
+ require 'nokogiri'
3
+ require 'css_parser'
4
+
5
+ module Roadie
6
+ # This module adds the Roadie functionality to ActionMailer 3 when included in ActionMailer::Base.
7
+ #
8
+ # If you want to add Roadie to any other mail framework, take a look at how this module is implemented.
9
+ module ActionMailerExtensions
10
+ def self.included(base)
11
+ base.class_eval do
12
+ if base.method_defined?(:collect_responses)
13
+ alias_method_chain :collect_responses, :inline_styles
14
+ else
15
+ alias_method_chain :collect_responses_and_parts_order, :inline_styles
16
+ end
17
+ alias_method_chain :mail, :inline_styles
18
+ end
19
+ end
20
+
21
+ protected
22
+ def mail_with_inline_styles(headers = {}, &block)
23
+ if headers.has_key?(:css)
24
+ @targets = headers[:css]
25
+ else
26
+ @targets = default_css_targets
27
+ end
28
+
29
+ if headers.has_key?(:after_inlining)
30
+ @after_inlining_handler = headers[:after_inlining]
31
+ else
32
+ @after_inlining_handler = default_after_inlining || Roadie.after_inlining_handler
33
+ end
34
+
35
+ mail_without_inline_styles(headers, &block).tap do |email|
36
+ email.header.fields.delete_if { |field| %w(css after_inlining).include?(field.name) }
37
+ end
38
+ end
39
+
40
+ # Rails 4
41
+ def collect_responses_with_inline_styles(headers, &block)
42
+ responses = collect_responses_without_inline_styles(headers, &block)
43
+ if Roadie.enabled?
44
+ responses.map { |response| inline_style_response(response) }
45
+ else
46
+ responses
47
+ end
48
+ end
49
+
50
+ # Rails 3
51
+ def collect_responses_and_parts_order_with_inline_styles(headers, &block)
52
+ responses, order = collect_responses_and_parts_order_without_inline_styles(headers, &block)
53
+ if Roadie.enabled?
54
+ [responses.map { |response| inline_style_response(response) }, order]
55
+ else
56
+ [responses, order]
57
+ end
58
+ end
59
+
60
+ private
61
+ def default_css_targets
62
+ self.class.default[:css]
63
+ end
64
+
65
+ def default_after_inlining
66
+ self.class.default[:after_inlining]
67
+ end
68
+
69
+ def after_inlining_handler
70
+ @after_inlining_handler
71
+ end
72
+
73
+ def inline_style_response(response)
74
+ if response[:content_type] == 'text/html'
75
+ response.merge :body => Roadie.inline_css(Roadie.current_provider, css_targets, response[:body], url_options, after_inlining_handler)
76
+ else
77
+ response
78
+ end
79
+ end
80
+
81
+ def css_targets
82
+ Array.wrap(@targets || []).map { |target| resolve_target(target) }.compact.map(&:to_s)
83
+ end
84
+
85
+ def resolve_target(target)
86
+ if target.kind_of? Proc
87
+ instance_exec(&target)
88
+ elsif target.respond_to? :call
89
+ target.call
90
+ else
91
+ target
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,28 @@
1
+ module Roadie
2
+ # A provider that hooks into Rail's Asset Pipeline.
3
+ #
4
+ # Usage:
5
+ # config.roadie.provider = AssetPipelineProvider.new('prefix')
6
+ #
7
+ # @see http://guides.rubyonrails.org/asset_pipeline.html
8
+ class AssetPipelineProvider < AssetProvider
9
+ # Looks up the file with the given name in the asset pipeline
10
+ #
11
+ # @return [String] contents of the file
12
+ def find(name)
13
+ asset_file(name).to_s.strip
14
+ end
15
+
16
+ private
17
+ def assets
18
+ Roadie.app.assets
19
+ end
20
+
21
+ def asset_file(name)
22
+ basename = remove_prefix(name)
23
+ assets[basename].tap do |file|
24
+ raise CSSFileNotFound.new(basename) unless file
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,62 @@
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)
17
+ 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
+ end
62
+ end
@@ -0,0 +1,22 @@
1
+ module Roadie
2
+ # Raised when a stylesheet specified for inlining is not present.
3
+ # You can access the target filename via #filename.
4
+ class CSSFileNotFound < StandardError
5
+ attr_reader :filename, :guess
6
+
7
+ def initialize(filename, guess = nil)
8
+ @filename = filename
9
+ @guess = guess
10
+ super(build_message)
11
+ end
12
+
13
+ private
14
+ def build_message
15
+ if guess
16
+ "Could not find #{filename} (guessed from #{guess.inspect})"
17
+ else
18
+ "Could not find #{filename}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,74 @@
1
+ require 'pathname'
2
+
3
+ module Roadie
4
+ # A provider that looks for files on the filesystem
5
+ #
6
+ # Usage:
7
+ # config.roadie.provider = FilesystemProvider.new("prefix", "path/to/stylesheets")
8
+ #
9
+ # Path specification follows certain rules thatare detailed in {#initialize}.
10
+ #
11
+ # @see #initialize
12
+ class FilesystemProvider < AssetProvider
13
+ # @return [Pathname] Pathname representing the directory of the assets
14
+ attr_reader :path
15
+
16
+ # Initializes a new instance of FilesystemProvider.
17
+ #
18
+ # The passed path can come in some variants:
19
+ # * +Pathname+ - will be used as-is
20
+ # * +String+ - If pointing to an absolute path, uses that path. If a relative path, relative from the +Rails.root+
21
+ # * +nil+ - Use the default path (equal to "public/stylesheets")
22
+ #
23
+ # @example Pointing to a directory in the project
24
+ # FilesystemProvider.new(Rails.root.join("public", "assets"))
25
+ # FilesystemProvider.new("public/assets")
26
+ #
27
+ # @example Pointing to external resource
28
+ # FilesystemProvider.new("/home/app/stuff")
29
+ #
30
+ # @param [String] prefix The prefix (see {#prefix})
31
+ # @param [String, Pathname, nil] path The path to use
32
+ def initialize(prefix = "/stylesheets", path = nil)
33
+ super(prefix)
34
+ if path
35
+ @path = resolve_path(path)
36
+ else
37
+ @path = default_path
38
+ end
39
+ end
40
+
41
+ # Looks for the file in the tree. If the file cannot be found, and it does not end with ".css", the lookup
42
+ # will be retried with ".css" appended to the filename.
43
+ #
44
+ # @return [String] contents of the file
45
+ def find(name)
46
+ base = remove_prefix(name)
47
+ file = path.join(base)
48
+ if file.exist?
49
+ file.read.strip
50
+ else
51
+ return find("#{base}.css") if base.to_s !~ /\.css$/
52
+ raise CSSFileNotFound.new(name, base.to_s)
53
+ end
54
+ end
55
+
56
+ private
57
+ def default_path
58
+ resolve_path("public/stylesheets")
59
+ end
60
+
61
+ def resolve_path(path)
62
+ if path.kind_of?(Pathname)
63
+ @path = path
64
+ else
65
+ pathname = Pathname.new(path)
66
+ if pathname.absolute?
67
+ @path = pathname
68
+ else
69
+ @path = Roadie.app.root.join(path)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,251 @@
1
+ require 'set'
2
+
3
+ module Roadie
4
+ # This class is the core of Roadie as it does all the actual work. You just give it
5
+ # the CSS rules, the HTML and the url_options for rewriting URLs and let it go on
6
+ # doing all the heavy lifting and building.
7
+ class Inliner
8
+ # Regexp matching all the url() declarations in CSS
9
+ #
10
+ # It matches without any quotes and with both single and double quotes
11
+ # inside the parenthesis. There's much room for improvement, of course.
12
+ CSS_URL_REGEXP = %r{
13
+ url\(
14
+ (
15
+ (?:["']|%22)? # Optional opening quote
16
+ )
17
+ (
18
+ [^(]* # Text leading up to before opening parens
19
+ (?:\([^)]*\))* # Texts containing parens pairs
20
+ [^(]+ # Texts without parens - required
21
+ )
22
+ \1 # Closing quote
23
+ \)
24
+ }x
25
+
26
+ # Initialize a new Inliner with the given Provider, CSS targets, HTML, and `url_options`.
27
+ #
28
+ # @param [AssetProvider] assets
29
+ # @param [Array] targets List of CSS files to load via the provider
30
+ # @param [String] html
31
+ # @param [Hash] url_options Supported keys: +:host+, +:port+, +:protocol+
32
+ # @param [lambda] after_inlining_handler A lambda that accepts one parameter or an object that responds to the +call+ method with one parameter.
33
+ def initialize(assets, targets, html, url_options, after_inlining_handler=nil, document_options={})
34
+ @assets = assets
35
+ @css = assets.all(targets)
36
+ @html = html
37
+ @inline_css = []
38
+ @url_options = url_options
39
+ @after_inlining_handler = after_inlining_handler
40
+ @document_options = document_options
41
+
42
+ if url_options and url_options[:asset_path_prefix]
43
+ raise ArgumentError, "The asset_path_prefix URL option is not working anymore. You need to add the following configuration to your application.rb:\n" +
44
+ " config.roadie.provider = AssetPipelineProvider.new(#{url_options[:asset_path_prefix].inspect})\n" +
45
+ "Note that the prefix \"/assets\" is the default one, so you do not need to configure anything in that case."
46
+ end
47
+ end
48
+
49
+ # Start the inlining and return the final HTML output
50
+ # @return [String]
51
+ def execute
52
+ adjust_html do |document|
53
+ @document = document
54
+ add_missing_structure
55
+ extract_link_elements
56
+ extract_inline_style_elements
57
+ inline_css_rules
58
+ make_image_urls_absolute
59
+ make_style_urls_absolute
60
+ after_inlining_handler.call(document) if after_inlining_handler.respond_to?(:call)
61
+ @document = nil
62
+ end
63
+ end
64
+
65
+ private
66
+ attr_reader :css, :html, :assets, :url_options, :document, :after_inlining_handler
67
+
68
+ def inline_css
69
+ @inline_css.join("\n")
70
+ end
71
+
72
+ def parsed_css
73
+ CssParser::Parser.new.tap do |parser|
74
+ parser.add_block! clean_css(css) if css
75
+ parser.add_block! clean_css(inline_css)
76
+ end
77
+ end
78
+
79
+ def adjust_html
80
+ parse_method = @document_options.key?(:fragment) ? :fragment : :parse
81
+
82
+ Nokogiri::HTML.__send__(parse_method, html).tap do |document|
83
+ yield document
84
+ end.dup.to_html
85
+ end
86
+
87
+ def add_missing_structure
88
+ html_node = document.at_css('html')
89
+ return unless html_node
90
+ html_node['xmlns'] ||= 'http://www.w3.org/1999/xhtml'
91
+
92
+ if document.at_css('html > head').present?
93
+ head = document.at_css('html > head')
94
+ else
95
+ head = Nokogiri::XML::Node.new('head', document)
96
+ document.at_css('html').children.before(head)
97
+ end
98
+
99
+ # This is handled automatically by Nokogiri in Ruby 1.9, IF charset of string != utf-8
100
+ # We want UTF-8 to be specified as well, so we still do this.
101
+ unless document.at_css('html > head > meta[http-equiv=Content-Type]')
102
+ meta = Nokogiri::XML::Node.new('meta', document)
103
+ meta['http-equiv'] = 'Content-Type'
104
+ meta['content'] = 'text/html; charset=UTF-8'
105
+ head.add_child(meta)
106
+ end
107
+ end
108
+
109
+ def extract_link_elements
110
+ all_link_elements_to_be_inlined_with_url.each do |link, url|
111
+ asset = assets.find(url.path)
112
+ @inline_css << asset.to_s
113
+ link.remove
114
+ end
115
+ end
116
+
117
+ def extract_inline_style_elements
118
+ document.css("style").each do |style|
119
+ next if style['media'] == 'print' or style['data-immutable']
120
+ @inline_css << style.content
121
+ style.remove
122
+ end
123
+ end
124
+
125
+ def inline_css_rules
126
+ elements_with_declarations.each do |element, declarations|
127
+ ordered_declarations = []
128
+ seen_properties = Set.new
129
+ declarations.sort.reverse_each do |declaration|
130
+ next if seen_properties.include?(declaration.property)
131
+ ordered_declarations.unshift(declaration)
132
+ seen_properties << declaration.property
133
+ end
134
+
135
+ rules_string = ordered_declarations.map { |declaration| declaration.to_s }.join(';')
136
+ element['style'] = [rules_string, element['style']].compact.join(';')
137
+ end
138
+ end
139
+
140
+ def elements_with_declarations
141
+ Hash.new { |hash, key| hash[key] = [] }.tap do |element_declarations|
142
+ parsed_css.each_rule_set do |rule_set, _|
143
+ each_good_selector(rule_set) do |selector|
144
+ each_element_in_selector(selector) do |element|
145
+ style_declarations_in_rule_set(selector.specificity, rule_set) do |declaration|
146
+ element_declarations[element] << declaration
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def each_good_selector(rules)
155
+ rules.selectors.each do |selector_string|
156
+ selector = Selector.new(selector_string)
157
+ yield selector if selector.inlinable?
158
+ end
159
+ end
160
+
161
+ def each_element_in_selector(selector)
162
+ document.css(selector.to_s).each do |element|
163
+ yield element
164
+ end
165
+ # There's no way to get a list of supported pseudo rules, so we're left
166
+ # with having to rescue errors.
167
+ # Pseudo selectors that are known to be bad are skipped automatically but
168
+ # this will catch the rest.
169
+ rescue Nokogiri::XML::XPath::SyntaxError, Nokogiri::CSS::SyntaxError => error
170
+ warn "Roadie cannot use #{selector.inspect} when inlining stylesheets"
171
+ rescue => error
172
+ warn "Roadie got error when looking for #{selector.inspect}: #{error}"
173
+ raise unless error.message.include?('XPath')
174
+ end
175
+
176
+ def style_declarations_in_rule_set(specificity, rule_set)
177
+ rule_set.each_declaration do |property, value, important|
178
+ yield StyleDeclaration.new(property, value, important, specificity)
179
+ end
180
+ end
181
+
182
+ def make_image_urls_absolute
183
+ document.css('img').each do |img|
184
+ img['src'] = ensure_absolute_url(img['src']) if img['src']
185
+ end
186
+ end
187
+
188
+ def make_style_urls_absolute
189
+ document.css('*[style]').each do |element|
190
+ styling = element['style']
191
+ element['style'] = styling.gsub(CSS_URL_REGEXP) { "url(#{$1}#{ensure_absolute_url($2, '/stylesheets')}#{$1})" }
192
+ end
193
+ end
194
+
195
+ def ensure_absolute_url(url, base_path = nil)
196
+ base, uri = absolute_url_base(base_path), URI.parse(url)
197
+ if uri.relative? and base
198
+ base.merge(uri).to_s
199
+ else
200
+ uri.to_s
201
+ end
202
+ rescue URI::InvalidURIError
203
+ return url
204
+ end
205
+
206
+ def absolute_url_base(base_path)
207
+ return nil unless url_options
208
+ port = url_options[:port]
209
+ scheme = protocol_to_scheme url_options[:protocol]
210
+ URI::Generic.build({
211
+ :scheme => scheme,
212
+ :host => url_options[:host],
213
+ :port => (port ? port.to_i : nil),
214
+ :path => base_path
215
+ })
216
+ end
217
+
218
+ # Strip :// from any protocol, if present
219
+ def protocol_to_scheme(protocol)
220
+ return 'http' unless protocol
221
+ protocol.to_s[/^\w+/]
222
+ end
223
+
224
+ def all_link_elements_with_url
225
+ document.css("link[rel=stylesheet]").map { |link| [link, URI.parse(link['href'])] }
226
+ end
227
+
228
+ def all_link_elements_to_be_inlined_with_url
229
+ all_link_elements_with_url.reject do |link, url|
230
+ absolute_path_url = (url.host or url.path.nil?)
231
+ blacklisted_element = (link['media'] == 'print' or link['data-immutable'])
232
+
233
+ absolute_path_url or blacklisted_element
234
+ end
235
+ end
236
+
237
+ CLEANING_MATCHER = /
238
+ (^\s* # Beginning-of-lines matches
239
+ (<!\[CDATA\[)|
240
+ (<!--+)
241
+ )|( # End-of-line matches
242
+ (--+>)|
243
+ (\]\]>)
244
+ $)
245
+ /x.freeze
246
+
247
+ def clean_css(css)
248
+ css.gsub(CLEANING_MATCHER, '')
249
+ end
250
+ end
251
+ end