roadie 3.0.5 → 3.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -7
- data/.travis.yml +1 -5
- data/Changelog.md +20 -1
- data/Guardfile +2 -1
- data/README.md +193 -53
- data/lib/roadie.rb +3 -2
- data/lib/roadie/asset_scanner.rb +30 -10
- data/lib/roadie/cached_provider.rb +77 -0
- data/lib/roadie/document.rb +29 -10
- data/lib/roadie/errors.rb +43 -1
- data/lib/roadie/filesystem_provider.rb +3 -1
- data/lib/roadie/inliner.rb +57 -27
- data/lib/roadie/markup_improver.rb +2 -1
- data/lib/roadie/net_http_provider.rb +70 -0
- data/lib/roadie/null_provider.rb +3 -0
- data/lib/roadie/path_rewriter_provider.rb +64 -0
- data/lib/roadie/provider_list.rb +19 -1
- data/lib/roadie/rspec.rb +1 -0
- data/lib/roadie/rspec/cache_store.rb +25 -0
- data/lib/roadie/stylesheet.rb +1 -0
- data/lib/roadie/url_generator.rb +2 -1
- data/lib/roadie/utils.rb +9 -0
- data/lib/roadie/version.rb +1 -1
- data/roadie.gemspec +2 -1
- data/spec/hash_as_cache_store_spec.rb +7 -0
- data/spec/integration_spec.rb +91 -0
- data/spec/lib/roadie/asset_scanner_spec.rb +79 -25
- data/spec/lib/roadie/cached_provider_spec.rb +52 -0
- data/spec/lib/roadie/document_spec.rb +43 -7
- data/spec/lib/roadie/filesystem_provider_spec.rb +5 -0
- data/spec/lib/roadie/inliner_spec.rb +72 -15
- data/spec/lib/roadie/net_http_provider_spec.rb +89 -0
- data/spec/lib/roadie/path_rewriter_provider_spec.rb +39 -0
- data/spec/lib/roadie/provider_list_spec.rb +31 -8
- data/spec/lib/roadie/stylesheet_spec.rb +14 -8
- data/spec/lib/roadie/utils_spec.rb +7 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/have_styling_matcher.rb +1 -0
- metadata +40 -9
- data/lib/roadie/upgrade_guide.rb +0 -36
data/lib/roadie.rb
CHANGED
@@ -16,6 +16,9 @@ require 'roadie/asset_provider'
|
|
16
16
|
require 'roadie/provider_list'
|
17
17
|
require 'roadie/filesystem_provider'
|
18
18
|
require 'roadie/null_provider'
|
19
|
+
require 'roadie/net_http_provider'
|
20
|
+
require 'roadie/cached_provider'
|
21
|
+
require 'roadie/path_rewriter_provider'
|
19
22
|
|
20
23
|
require 'roadie/asset_scanner'
|
21
24
|
require 'roadie/markup_improver'
|
@@ -24,5 +27,3 @@ require 'roadie/url_rewriter'
|
|
24
27
|
require 'roadie/null_url_rewriter'
|
25
28
|
require 'roadie/inliner'
|
26
29
|
require 'roadie/document'
|
27
|
-
|
28
|
-
require 'roadie/upgrade_guide'
|
data/lib/roadie/asset_scanner.rb
CHANGED
@@ -5,15 +5,18 @@ module Roadie
|
|
5
5
|
# DOM tree. Referenced styles will be found using the provided asset
|
6
6
|
# provider.
|
7
7
|
#
|
8
|
-
# Any style declaration tagged with +data-roadie-ignore+ will be ignored
|
8
|
+
# Any style declaration tagged with +data-roadie-ignore+ will be ignored,
|
9
|
+
# except for having the attribute itself removed.
|
9
10
|
class AssetScanner
|
10
|
-
attr_reader :dom, :
|
11
|
+
attr_reader :dom, :normal_asset_provider, :external_asset_provider
|
11
12
|
|
12
13
|
# @param [Nokogiri::HTML::Document] dom
|
13
|
-
# @param [#find_stylesheet!]
|
14
|
-
|
14
|
+
# @param [#find_stylesheet!] normal_asset_provider
|
15
|
+
# @param [#find_stylesheet!] external_asset_provider
|
16
|
+
def initialize(dom, normal_asset_provider, external_asset_provider)
|
15
17
|
@dom = dom
|
16
|
-
@
|
18
|
+
@normal_asset_provider = normal_asset_provider
|
19
|
+
@external_asset_provider = external_asset_provider
|
17
20
|
end
|
18
21
|
|
19
22
|
# Looks for all non-ignored stylesheets and returns them.
|
@@ -38,15 +41,16 @@ module Roadie
|
|
38
41
|
# @see #find_css
|
39
42
|
# @return [Enumerable<Stylesheet>] every extracted stylesheet
|
40
43
|
def extract_css
|
41
|
-
@dom.css(STYLE_ELEMENT_QUERY).map { |element|
|
44
|
+
stylesheets = @dom.css(STYLE_ELEMENT_QUERY).map { |element|
|
42
45
|
stylesheet = read_stylesheet(element)
|
43
46
|
element.remove if stylesheet
|
44
47
|
stylesheet
|
45
48
|
}.compact
|
49
|
+
remove_ignore_markers
|
50
|
+
stylesheets
|
46
51
|
end
|
47
52
|
|
48
53
|
private
|
49
|
-
|
50
54
|
STYLE_ELEMENT_QUERY = (
|
51
55
|
"style:not([data-roadie-ignore]), " +
|
52
56
|
# TODO: When using Nokogiri 1.6.1 and later; we may use a double :not here
|
@@ -70,7 +74,7 @@ module Roadie
|
|
70
74
|
def read_stylesheet(element)
|
71
75
|
if element.name == "style"
|
72
76
|
read_style_element element
|
73
|
-
|
77
|
+
elsif element.name == "link" && element['media'] != "print" && element["href"]
|
74
78
|
read_link_element element
|
75
79
|
end
|
76
80
|
end
|
@@ -80,13 +84,29 @@ module Roadie
|
|
80
84
|
end
|
81
85
|
|
82
86
|
def read_link_element(element)
|
83
|
-
if
|
84
|
-
|
87
|
+
if Utils.path_is_absolute?(element["href"])
|
88
|
+
external_asset_provider.find_stylesheet! element['href'] if should_find_external?
|
89
|
+
else
|
90
|
+
normal_asset_provider.find_stylesheet! element['href']
|
85
91
|
end
|
86
92
|
end
|
87
93
|
|
88
94
|
def clean_css(css)
|
89
95
|
css.gsub(CLEANING_MATCHER, '')
|
90
96
|
end
|
97
|
+
|
98
|
+
def should_find_external?
|
99
|
+
return false unless external_asset_provider
|
100
|
+
# If external_asset_provider is empty list; don't use it.
|
101
|
+
return false if external_asset_provider.respond_to?(:empty?) && external_asset_provider.empty?
|
102
|
+
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def remove_ignore_markers
|
107
|
+
@dom.css("[data-roadie-ignore]").each do |node|
|
108
|
+
node.remove_attribute "data-roadie-ignore"
|
109
|
+
end
|
110
|
+
end
|
91
111
|
end
|
92
112
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Roadie
|
2
|
+
# @api public
|
3
|
+
# The {CachedProvider} wraps another provider (or {ProviderList}) and caches
|
4
|
+
# the response from it.
|
5
|
+
#
|
6
|
+
# The default cache store is a instance-specific hash that lives for the
|
7
|
+
# entire duration of the instance. If you want to share hash between
|
8
|
+
# instances, pass your own hash-like object. Just remember to not allow this
|
9
|
+
# cache to grow without bounds, which a shared hash would do.
|
10
|
+
#
|
11
|
+
# Not found assets are not cached currently, but it's possible to extend this
|
12
|
+
# class in the future if there is a need for it. Remember this if you have
|
13
|
+
# providers with very slow failures.
|
14
|
+
#
|
15
|
+
# The cache store must accept {Roadie::Stylesheet} instances, and return such
|
16
|
+
# instances when fetched. It must respond to `#[name]` and `#[name]=` to
|
17
|
+
# retrieve and set entries, respectively. The `#[name]=` method also needs to
|
18
|
+
# return the instance again.
|
19
|
+
#
|
20
|
+
# @example Global cache
|
21
|
+
# Application.asset_cache = Hash.new
|
22
|
+
# slow_provider = MyDatabaseProvider.new(Application)
|
23
|
+
# provider = Roadie::CachedProvider.new(slow_provider, Application.asset_cache)
|
24
|
+
#
|
25
|
+
# @example Custom cache store
|
26
|
+
# class MyRoadieMemcacheStore
|
27
|
+
# def initialize(memcache)
|
28
|
+
# @memcache = memcache
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def [](path)
|
32
|
+
# css = memcache.read("assets/#{path}/css")
|
33
|
+
# if css
|
34
|
+
# name = memcache.read("assets/#{path}/name") || "cached #{path}"
|
35
|
+
# Roadie::Stylesheet.new(name, css)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def []=(path, stylesheet)
|
40
|
+
# memcache.write("assets/#{path}/css", stylesheet.to_s)
|
41
|
+
# memcache.write("assets/#{path}/name", stylesheet.name)
|
42
|
+
# stylesheet # You need to return the set Stylesheet
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
class CachedProvider
|
47
|
+
# The cache store used by this instance.
|
48
|
+
attr_reader :cache
|
49
|
+
|
50
|
+
# @param upstream [an asset provider] The wrapped asset provider
|
51
|
+
# @param cache [#[], #[]=] The cache store to use.
|
52
|
+
def initialize(upstream, cache = {})
|
53
|
+
@upstream = upstream
|
54
|
+
@cache = cache
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_stylesheet(name)
|
58
|
+
cache_fetch(name) do
|
59
|
+
@upstream.find_stylesheet(name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_stylesheet!(name)
|
64
|
+
cache_fetch(name) do
|
65
|
+
@upstream.find_stylesheet!(name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def cache_fetch(name)
|
71
|
+
cache[name] || cache[name] = yield
|
72
|
+
rescue CssNotFound
|
73
|
+
cache[name] = nil
|
74
|
+
raise
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/roadie/document.rb
CHANGED
@@ -13,10 +13,10 @@ module Roadie
|
|
13
13
|
# The internal stylesheet is used last and gets the highest priority. The
|
14
14
|
# rest is used in the same order as browsers are supposed to use them.
|
15
15
|
#
|
16
|
-
# @attr [#call] before_transformation Callback to call just before {#transform}ation
|
17
|
-
# @attr [#call] after_transformation Callback to call just before {#transform}ation is completed. Will be called with the current DOM tree.
|
16
|
+
# @attr [#call] before_transformation Callback to call just before {#transform}ation begins. Will be called with the parsed DOM tree and the {Document} instance.
|
17
|
+
# @attr [#call] after_transformation Callback to call just before {#transform}ation is completed. Will be called with the current DOM tree and the {Document} instance.
|
18
18
|
class Document
|
19
|
-
attr_reader :html, :asset_providers
|
19
|
+
attr_reader :html, :asset_providers, :external_asset_providers
|
20
20
|
|
21
21
|
# URL options. If none are given no URL rewriting will take place.
|
22
22
|
# @see UrlGenerator#initialize
|
@@ -24,10 +24,15 @@ module Roadie
|
|
24
24
|
|
25
25
|
attr_accessor :before_transformation, :after_transformation
|
26
26
|
|
27
|
+
# Should CSS that cannot be inlined be kept in a new `<style>` element in `<head>`?
|
28
|
+
attr_accessor :keep_uninlinable_css
|
29
|
+
|
27
30
|
# @param [String] html the input HTML
|
28
31
|
def initialize(html)
|
32
|
+
@keep_uninlinable_css = true
|
29
33
|
@html = html
|
30
34
|
@asset_providers = ProviderList.wrap(FilesystemProvider.new)
|
35
|
+
@external_asset_providers = ProviderList.empty
|
31
36
|
@css = ""
|
32
37
|
end
|
33
38
|
|
@@ -39,9 +44,10 @@ module Roadie
|
|
39
44
|
|
40
45
|
# Transform the input HTML and returns the processed HTML.
|
41
46
|
#
|
42
|
-
# Before the transformation begins, the {#before_transformation} callback
|
43
|
-
# called with the parsed HTML tree
|
44
|
-
# {#after_transformation} callback will be
|
47
|
+
# Before the transformation begins, the {#before_transformation} callback
|
48
|
+
# will be called with the parsed HTML tree and the {Document} instance, and
|
49
|
+
# after all work is complete the {#after_transformation} callback will be
|
50
|
+
# invoked in the same way.
|
45
51
|
#
|
46
52
|
# Most of the work is delegated to other classes. A list of them can be seen below.
|
47
53
|
#
|
@@ -65,11 +71,16 @@ module Roadie
|
|
65
71
|
dom.dup.to_html
|
66
72
|
end
|
67
73
|
|
68
|
-
# Assign new asset providers. The supplied list will be wrapped in a {ProviderList} using {ProviderList.wrap}.
|
74
|
+
# Assign new normal asset providers. The supplied list will be wrapped in a {ProviderList} using {ProviderList.wrap}.
|
69
75
|
def asset_providers=(list)
|
70
76
|
@asset_providers = ProviderList.wrap(list)
|
71
77
|
end
|
72
78
|
|
79
|
+
# Assign new external asset providers. The supplied list will be wrapped in a {ProviderList} using {ProviderList.wrap}.
|
80
|
+
def external_asset_providers=(list)
|
81
|
+
@external_asset_providers = ProviderList.wrap(list)
|
82
|
+
end
|
83
|
+
|
73
84
|
private
|
74
85
|
def stylesheet
|
75
86
|
Stylesheet.new "(Document styles)", @css
|
@@ -80,8 +91,8 @@ module Roadie
|
|
80
91
|
end
|
81
92
|
|
82
93
|
def inline(dom)
|
83
|
-
dom_stylesheets = AssetScanner.new(dom, asset_providers).extract_css
|
84
|
-
Inliner.new(dom_stylesheets + [stylesheet]).inline(
|
94
|
+
dom_stylesheets = AssetScanner.new(dom, asset_providers, external_asset_providers).extract_css
|
95
|
+
Inliner.new(dom_stylesheets + [stylesheet], dom).inline(keep_uninlinable_css)
|
85
96
|
end
|
86
97
|
|
87
98
|
def rewrite_urls(dom)
|
@@ -97,7 +108,15 @@ module Roadie
|
|
97
108
|
end
|
98
109
|
|
99
110
|
def callback(callable, dom)
|
100
|
-
|
111
|
+
if callable.respond_to?(:call)
|
112
|
+
# Arity checking is to support the API without bumping a major version.
|
113
|
+
# TODO: Remove on next major version (v4.0)
|
114
|
+
if !callable.respond_to?(:parameters) || callable.parameters.size == 1
|
115
|
+
callable.(dom)
|
116
|
+
else
|
117
|
+
callable.(dom, self)
|
118
|
+
end
|
119
|
+
end
|
101
120
|
end
|
102
121
|
end
|
103
122
|
end
|
data/lib/roadie/errors.rb
CHANGED
@@ -38,19 +38,61 @@ module Roadie
|
|
38
38
|
# Provider used when finding
|
39
39
|
attr_reader :provider
|
40
40
|
|
41
|
+
# Extra message
|
42
|
+
attr_reader :extra_message
|
43
|
+
|
41
44
|
# TODO: Change signature in the next major version of Roadie.
|
42
45
|
def initialize(css_name, extra_message = nil, provider = nil)
|
43
46
|
@css_name = css_name
|
44
47
|
@provider = provider
|
48
|
+
@extra_message = extra_message
|
45
49
|
super build_message(extra_message)
|
46
50
|
end
|
47
51
|
|
52
|
+
protected
|
53
|
+
def error_row
|
54
|
+
"#{provider || "Unknown provider"}: #{extra_message || message}"
|
55
|
+
end
|
56
|
+
|
48
57
|
private
|
49
|
-
|
58
|
+
# Redundant method argument is to keep API compatability without major version bump.
|
59
|
+
# TODO: Remove argument on version 4.0.
|
60
|
+
def build_message(extra_message = @extra_message)
|
50
61
|
message = %(Could not find stylesheet "#{css_name}")
|
51
62
|
message << ": #{extra_message}" if extra_message
|
52
63
|
message << "\nUsed provider:\n#{provider}" if provider
|
53
64
|
message
|
54
65
|
end
|
55
66
|
end
|
67
|
+
|
68
|
+
class ProvidersFailed < CssNotFound
|
69
|
+
attr_reader :errors
|
70
|
+
|
71
|
+
def initialize(css_name, provider_list, errors)
|
72
|
+
@errors = errors
|
73
|
+
super(css_name, "All providers failed", provider_list)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def build_message(extra_message)
|
78
|
+
message = %(Could not find stylesheet "#{css_name}": #{extra_message}\nUsed providers:\n)
|
79
|
+
each_error_row(errors) do |row|
|
80
|
+
message << "\t" << row << "\n"
|
81
|
+
end
|
82
|
+
message
|
83
|
+
end
|
84
|
+
|
85
|
+
def each_error_row(errors)
|
86
|
+
errors.each do |error|
|
87
|
+
case error
|
88
|
+
when ProvidersFailed
|
89
|
+
each_error_row(error.errors) { |row| yield row }
|
90
|
+
when CssNotFound
|
91
|
+
yield error.error_row
|
92
|
+
else
|
93
|
+
yield "Unknown provider (#{error.class}): #{error}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
56
98
|
end
|
@@ -8,7 +8,6 @@ module Roadie
|
|
8
8
|
# the base path.
|
9
9
|
class InsecurePathError < Error; end
|
10
10
|
|
11
|
-
include AssetProvider
|
12
11
|
attr_reader :path
|
13
12
|
|
14
13
|
def initialize(path = Dir.pwd)
|
@@ -35,6 +34,9 @@ module Roadie
|
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
37
|
+
def to_s() inspect end
|
38
|
+
def inspect() "#<#{self.class} #@path>" end
|
39
|
+
|
38
40
|
private
|
39
41
|
def build_file_path(name)
|
40
42
|
raise InsecurePathError, name if name.include?("..")
|
data/lib/roadie/inliner.rb
CHANGED
@@ -16,64 +16,94 @@ module Roadie
|
|
16
16
|
# <a href="/" style="color:red"></a>
|
17
17
|
class Inliner
|
18
18
|
# @param [Array<Stylesheet>] stylesheets the stylesheets to use in the inlining
|
19
|
-
|
19
|
+
# @param [Nokogiri::HTML::Document] dom
|
20
|
+
def initialize(stylesheets, dom)
|
20
21
|
@stylesheets = stylesheets
|
22
|
+
@dom = dom
|
21
23
|
end
|
22
24
|
|
23
25
|
# Start the inlining, mutating the DOM tree.
|
24
26
|
#
|
25
|
-
# @param [
|
27
|
+
# @param [true, false] keep_extra_blocks
|
26
28
|
# @return [nil]
|
27
|
-
def inline(
|
28
|
-
|
29
|
-
nil
|
30
|
-
end
|
29
|
+
def inline(keep_extra_blocks = true)
|
30
|
+
style_map, extra_blocks = consume_stylesheets
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
apply_style_map(style_map)
|
33
|
+
add_styles_to_head(extra_blocks) if keep_extra_blocks
|
34
34
|
|
35
|
-
|
36
|
-
style_map.each_element do |element, builder|
|
37
|
-
apply_element_style element, builder
|
38
|
-
end
|
35
|
+
nil
|
39
36
|
end
|
40
37
|
|
41
|
-
|
38
|
+
protected
|
39
|
+
attr_reader :stylesheets, :dom
|
40
|
+
|
41
|
+
private
|
42
|
+
def consume_stylesheets
|
42
43
|
style_map = StyleMap.new
|
44
|
+
extra_blocks = []
|
43
45
|
|
44
|
-
|
45
|
-
elements =
|
46
|
-
|
46
|
+
each_style_block do |stylesheet, block|
|
47
|
+
if (elements = selector_elements(stylesheet, block))
|
48
|
+
style_map.add elements, block.properties
|
49
|
+
else
|
50
|
+
extra_blocks << block
|
51
|
+
end
|
47
52
|
end
|
48
53
|
|
49
|
-
style_map
|
54
|
+
[style_map, extra_blocks]
|
50
55
|
end
|
51
56
|
|
52
|
-
def
|
57
|
+
def each_style_block
|
53
58
|
stylesheets.each do |stylesheet|
|
54
|
-
stylesheet.
|
55
|
-
yield stylesheet,
|
59
|
+
stylesheet.blocks.each do |block|
|
60
|
+
yield stylesheet, block
|
56
61
|
end
|
57
62
|
end
|
58
63
|
end
|
59
64
|
|
65
|
+
def selector_elements(stylesheet, block)
|
66
|
+
block.inlinable? && elements_matching_selector(stylesheet, block.selector)
|
67
|
+
end
|
68
|
+
|
69
|
+
def apply_style_map(style_map)
|
70
|
+
style_map.each_element { |element, builder| apply_element_style(element, builder) }
|
71
|
+
end
|
72
|
+
|
60
73
|
def apply_element_style(element, builder)
|
61
74
|
element["style"] = [builder.attribute_string, element["style"]].compact.join(";")
|
62
75
|
end
|
63
76
|
|
64
|
-
def elements_matching_selector(stylesheet, selector
|
77
|
+
def elements_matching_selector(stylesheet, selector)
|
65
78
|
dom.css(selector.to_s)
|
66
79
|
# There's no way to get a list of supported pseudo selectors, so we're left
|
67
80
|
# with having to rescue errors.
|
68
81
|
# Pseudo selectors that are known to be bad are skipped automatically but
|
69
82
|
# this will catch the rest.
|
70
83
|
rescue Nokogiri::XML::XPath::SyntaxError, Nokogiri::CSS::SyntaxError => error
|
71
|
-
warn "
|
72
|
-
|
84
|
+
Utils.warn "Cannot inline #{selector.inspect} from \"#{stylesheet.name}\" stylesheet. If this is valid CSS, please report a bug."
|
85
|
+
nil
|
73
86
|
rescue => error
|
74
|
-
warn "
|
87
|
+
Utils.warn "Got error when looking for #{selector.inspect} (from \"#{stylesheet.name}\" stylesheet): #{error}"
|
75
88
|
raise unless error.message.include?('XPath')
|
76
|
-
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_styles_to_head(blocks)
|
93
|
+
unless blocks.empty?
|
94
|
+
create_style_element(blocks, find_head)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_head
|
99
|
+
dom.at_xpath('html/head')
|
100
|
+
end
|
101
|
+
|
102
|
+
def create_style_element(style_blocks, head)
|
103
|
+
return unless head
|
104
|
+
element = Nokogiri::XML::Node.new("style", head.document)
|
105
|
+
element.content = style_blocks.join("\n")
|
106
|
+
head.add_child(element)
|
77
107
|
end
|
78
108
|
|
79
109
|
# @api private
|
@@ -94,8 +124,8 @@ module Roadie
|
|
94
124
|
end
|
95
125
|
end
|
96
126
|
|
97
|
-
def each_element
|
98
|
-
@map.each_pair
|
127
|
+
def each_element(&block)
|
128
|
+
@map.each_pair(&block)
|
99
129
|
end
|
100
130
|
end
|
101
131
|
end
|