md-roadie 2.4.2.md.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +10 -0
- data/.gitignore +12 -0
- data/.travis.yml +22 -0
- data/.yardopts +1 -0
- data/Appraisals +15 -0
- data/Changelog.md +185 -0
- data/Gemfile +11 -0
- data/Guardfile +8 -0
- data/MIT-LICENSE +20 -0
- data/README.md +310 -0
- data/Rakefile +30 -0
- data/autotest/discover.rb +1 -0
- data/gemfiles/rails_3.0.gemfile +7 -0
- data/gemfiles/rails_3.0.gemfile.lock +123 -0
- data/gemfiles/rails_3.1.gemfile +7 -0
- data/gemfiles/rails_3.1.gemfile.lock +126 -0
- data/gemfiles/rails_3.2.gemfile +7 -0
- data/gemfiles/rails_3.2.gemfile.lock +124 -0
- data/gemfiles/rails_4.0.gemfile +7 -0
- data/gemfiles/rails_4.0.gemfile.lock +119 -0
- data/lib/roadie.rb +79 -0
- data/lib/roadie/action_mailer_extensions.rb +95 -0
- data/lib/roadie/asset_pipeline_provider.rb +28 -0
- data/lib/roadie/asset_provider.rb +62 -0
- data/lib/roadie/css_file_not_found.rb +22 -0
- data/lib/roadie/filesystem_provider.rb +74 -0
- data/lib/roadie/inliner.rb +251 -0
- data/lib/roadie/railtie.rb +39 -0
- data/lib/roadie/selector.rb +50 -0
- data/lib/roadie/style_declaration.rb +42 -0
- data/lib/roadie/version.rb +3 -0
- data/md-roadie.gemspec +36 -0
- data/spec/fixtures/app/assets/stylesheets/integration.css +10 -0
- data/spec/fixtures/public/stylesheets/integration.css +10 -0
- data/spec/fixtures/views/integration_mailer/marketing.html.erb +2 -0
- data/spec/fixtures/views/integration_mailer/notification.html.erb +8 -0
- data/spec/fixtures/views/integration_mailer/notification.text.erb +6 -0
- data/spec/integration_spec.rb +110 -0
- data/spec/lib/roadie/action_mailer_extensions_spec.rb +227 -0
- data/spec/lib/roadie/asset_pipeline_provider_spec.rb +65 -0
- data/spec/lib/roadie/css_file_not_found_spec.rb +29 -0
- data/spec/lib/roadie/filesystem_provider_spec.rb +94 -0
- data/spec/lib/roadie/inliner_spec.rb +591 -0
- data/spec/lib/roadie/selector_spec.rb +55 -0
- data/spec/lib/roadie/style_declaration_spec.rb +49 -0
- data/spec/lib/roadie_spec.rb +101 -0
- data/spec/shared_examples/asset_provider_examples.rb +11 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/support/anonymous_mailer.rb +21 -0
- data/spec/support/change_url_options.rb +5 -0
- data/spec/support/have_attribute_matcher.rb +28 -0
- data/spec/support/have_node_matcher.rb +19 -0
- data/spec/support/have_selector_matcher.rb +6 -0
- data/spec/support/have_styling_matcher.rb +25 -0
- data/spec/support/parse_styling.rb +25 -0
- metadata +318 -0
data/lib/roadie.rb
ADDED
@@ -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
|