roadie 2.4.3 → 3.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +10 -14
- data/.yardopts +1 -1
- data/Changelog.md +38 -5
- data/Gemfile +3 -4
- data/Guardfile +12 -1
- data/README.md +168 -164
- data/Rakefile +2 -19
- data/lib/roadie.rb +15 -68
- data/lib/roadie/asset_provider.rb +7 -58
- data/lib/roadie/asset_scanner.rb +92 -0
- data/lib/roadie/document.rb +103 -0
- data/lib/roadie/errors.rb +57 -0
- data/lib/roadie/filesystem_provider.rb +30 -60
- data/lib/roadie/inliner.rb +72 -217
- data/lib/roadie/markup_improver.rb +88 -0
- data/lib/roadie/null_provider.rb +13 -0
- data/lib/roadie/null_url_rewriter.rb +12 -0
- data/lib/roadie/provider_list.rb +71 -0
- data/lib/roadie/rspec.rb +1 -0
- data/lib/roadie/rspec/asset_provider.rb +49 -0
- data/lib/roadie/selector.rb +43 -18
- data/lib/roadie/style_attribute_builder.rb +25 -0
- data/lib/roadie/style_block.rb +32 -0
- data/lib/roadie/style_property.rb +93 -0
- data/lib/roadie/stylesheet.rb +65 -0
- data/lib/roadie/upgrade_guide.rb +36 -0
- data/lib/roadie/url_generator.rb +126 -0
- data/lib/roadie/url_rewriter.rb +84 -0
- data/lib/roadie/version.rb +1 -1
- data/roadie.gemspec +8 -11
- data/spec/fixtures/big_em.css +1 -0
- data/spec/fixtures/stylesheets/green.css +1 -0
- data/spec/integration_spec.rb +125 -95
- data/spec/lib/roadie/asset_scanner_spec.rb +153 -0
- data/spec/lib/roadie/css_not_found_spec.rb +17 -0
- data/spec/lib/roadie/document_spec.rb +123 -0
- data/spec/lib/roadie/filesystem_provider_spec.rb +44 -68
- data/spec/lib/roadie/inliner_spec.rb +105 -537
- data/spec/lib/roadie/markup_improver_spec.rb +78 -0
- data/spec/lib/roadie/null_provider_spec.rb +21 -0
- data/spec/lib/roadie/null_url_rewriter_spec.rb +19 -0
- data/spec/lib/roadie/provider_list_spec.rb +89 -0
- data/spec/lib/roadie/selector_spec.rb +15 -10
- data/spec/lib/roadie/style_attribute_builder_spec.rb +29 -0
- data/spec/lib/roadie/style_block_spec.rb +35 -0
- data/spec/lib/roadie/style_property_spec.rb +82 -0
- data/spec/lib/roadie/stylesheet_spec.rb +41 -0
- data/spec/lib/roadie/test_provider_spec.rb +29 -0
- data/spec/lib/roadie/url_generator_spec.rb +121 -0
- data/spec/lib/roadie/url_rewriter_spec.rb +79 -0
- data/spec/shared_examples/asset_provider.rb +11 -0
- data/spec/shared_examples/url_rewriter.rb +23 -0
- data/spec/spec_helper.rb +6 -60
- data/spec/support/have_attribute_matcher.rb +2 -2
- data/spec/support/have_node_matcher.rb +4 -4
- data/spec/support/have_selector_matcher.rb +3 -3
- data/spec/support/have_styling_matcher.rb +51 -15
- data/spec/support/test_provider.rb +13 -0
- metadata +86 -175
- data/Appraisals +0 -15
- data/gemfiles/rails_3.0.gemfile +0 -7
- data/gemfiles/rails_3.0.gemfile.lock +0 -123
- data/gemfiles/rails_3.1.gemfile +0 -7
- data/gemfiles/rails_3.1.gemfile.lock +0 -126
- data/gemfiles/rails_3.2.gemfile +0 -7
- data/gemfiles/rails_3.2.gemfile.lock +0 -124
- data/gemfiles/rails_4.0.gemfile +0 -7
- data/gemfiles/rails_4.0.gemfile.lock +0 -119
- data/lib/roadie/action_mailer_extensions.rb +0 -95
- data/lib/roadie/asset_pipeline_provider.rb +0 -28
- data/lib/roadie/css_file_not_found.rb +0 -22
- data/lib/roadie/railtie.rb +0 -39
- data/lib/roadie/style_declaration.rb +0 -42
- data/spec/fixtures/app/assets/stylesheets/integration.css +0 -10
- data/spec/fixtures/public/stylesheets/integration.css +0 -10
- data/spec/fixtures/views/integration_mailer/marketing.html.erb +0 -2
- data/spec/fixtures/views/integration_mailer/notification.html.erb +0 -8
- data/spec/fixtures/views/integration_mailer/notification.text.erb +0 -6
- data/spec/lib/roadie/action_mailer_extensions_spec.rb +0 -227
- data/spec/lib/roadie/asset_pipeline_provider_spec.rb +0 -65
- data/spec/lib/roadie/css_file_not_found_spec.rb +0 -29
- data/spec/lib/roadie/style_declaration_spec.rb +0 -49
- data/spec/lib/roadie_spec.rb +0 -101
- data/spec/shared_examples/asset_provider_examples.rb +0 -11
- data/spec/support/anonymous_mailer.rb +0 -21
- data/spec/support/change_url_options.rb +0 -5
- data/spec/support/parse_styling.rb +0 -25
@@ -1,74 +1,44 @@
|
|
1
|
-
require 'pathname'
|
2
|
-
|
3
1
|
module Roadie
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# Usage:
|
7
|
-
# config.roadie.provider = FilesystemProvider.new("prefix", "path/to/stylesheets")
|
8
|
-
#
|
9
|
-
# Path specification follows certain rules thatare detailed in {#initialize}.
|
2
|
+
# Asset provider that looks for files on your local filesystem.
|
10
3
|
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
4
|
+
# It will be locked to a specific path and it will not access files above
|
5
|
+
# that directory.
|
6
|
+
class FilesystemProvider
|
7
|
+
# Raised when FilesystemProvider is asked to access a file that lies above
|
8
|
+
# the base path.
|
9
|
+
class InsecurePathError < Error; end
|
10
|
+
|
11
|
+
include AssetProvider
|
14
12
|
attr_reader :path
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
14
|
+
def initialize(path = Dir.pwd)
|
15
|
+
@path = path
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Stylesheet, nil]
|
19
|
+
def find_stylesheet(name)
|
20
|
+
file_path = build_file_path(name)
|
21
|
+
if File.exist? file_path
|
22
|
+
Stylesheet.new file_path, File.read(file_path)
|
38
23
|
end
|
39
24
|
end
|
40
25
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
file = path.join(base)
|
48
|
-
if file.exist?
|
49
|
-
file.read.strip
|
26
|
+
# @raise InsecurePathError
|
27
|
+
# @return [Stylesheet]
|
28
|
+
def find_stylesheet!(name)
|
29
|
+
file_path = build_file_path(name)
|
30
|
+
if File.exist? file_path
|
31
|
+
Stylesheet.new file_path, File.read(file_path)
|
50
32
|
else
|
51
|
-
|
52
|
-
raise
|
33
|
+
clean_name = File.basename file_path
|
34
|
+
raise CssNotFound.new(clean_name, %{#{file_path} does not exist. (Original name was "#{name}")})
|
53
35
|
end
|
54
36
|
end
|
55
37
|
|
56
38
|
private
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
39
|
+
def build_file_path(name)
|
40
|
+
raise InsecurePathError, name if name.include?("..")
|
41
|
+
File.join(@path, name[/^([^?]+)/])
|
42
|
+
end
|
73
43
|
end
|
74
44
|
end
|
data/lib/roadie/inliner.rb
CHANGED
@@ -1,247 +1,102 @@
|
|
1
1
|
require 'set'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'uri'
|
4
|
+
require 'css_parser'
|
2
5
|
|
3
6
|
module Roadie
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
+
# @api private
|
8
|
+
# The Inliner inlines stylesheets to the elements of the DOM.
|
9
|
+
#
|
10
|
+
# Inlining means that {StyleBlock}s and a DOM tree are combined:
|
11
|
+
# a { color: red; } # StyleBlock
|
12
|
+
# <a href="/"></a> # DOM
|
13
|
+
#
|
14
|
+
# becomes
|
15
|
+
#
|
16
|
+
# <a href="/" style="color:red"></a>
|
7
17
|
class Inliner
|
8
|
-
#
|
9
|
-
|
10
|
-
|
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)
|
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
|
-
|
41
|
-
if url_options and url_options[:asset_path_prefix]
|
42
|
-
raise ArgumentError, "The asset_path_prefix URL option is not working anymore. You need to add the following configuration to your application.rb:\n" +
|
43
|
-
" config.roadie.provider = AssetPipelineProvider.new(#{url_options[:asset_path_prefix].inspect})\n" +
|
44
|
-
"Note that the prefix \"/assets\" is the default one, so you do not need to configure anything in that case."
|
45
|
-
end
|
18
|
+
# @param [Array<Stylesheet>] stylesheets the stylesheets to use in the inlining
|
19
|
+
def initialize(stylesheets)
|
20
|
+
@stylesheets = stylesheets
|
46
21
|
end
|
47
22
|
|
48
|
-
# Start the inlining
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
extract_inline_style_elements
|
56
|
-
inline_css_rules
|
57
|
-
make_image_urls_absolute
|
58
|
-
make_style_urls_absolute
|
59
|
-
after_inlining_handler.call(document) if after_inlining_handler.respond_to?(:call)
|
60
|
-
@document = nil
|
61
|
-
end
|
23
|
+
# Start the inlining, mutating the DOM tree.
|
24
|
+
#
|
25
|
+
# @param [Nokogiri::HTML::Document] dom
|
26
|
+
# @return [nil]
|
27
|
+
def inline(dom)
|
28
|
+
apply style_map(dom)
|
29
|
+
nil
|
62
30
|
end
|
63
31
|
|
64
32
|
private
|
65
|
-
|
33
|
+
attr_reader :stylesheets
|
66
34
|
|
67
|
-
|
68
|
-
|
35
|
+
def apply(style_map)
|
36
|
+
style_map.each_element do |element, builder|
|
37
|
+
apply_element_style element, builder
|
69
38
|
end
|
39
|
+
end
|
70
40
|
|
71
|
-
|
72
|
-
|
73
|
-
parser.add_block! clean_css(css) if css
|
74
|
-
parser.add_block! clean_css(inline_css)
|
75
|
-
end
|
76
|
-
end
|
41
|
+
def style_map(dom)
|
42
|
+
style_map = StyleMap.new
|
77
43
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
end.dup.to_html
|
44
|
+
each_inlinable_block do |stylesheet, selector, properties|
|
45
|
+
elements = elements_matching_selector(stylesheet, selector, dom)
|
46
|
+
style_map.add elements, properties
|
82
47
|
end
|
83
48
|
|
84
|
-
|
85
|
-
|
86
|
-
html_node['xmlns'] ||= 'http://www.w3.org/1999/xhtml'
|
87
|
-
|
88
|
-
if document.at_css('html > head').present?
|
89
|
-
head = document.at_css('html > head')
|
90
|
-
else
|
91
|
-
head = Nokogiri::XML::Node.new('head', document)
|
92
|
-
document.at_css('html').children.before(head)
|
93
|
-
end
|
94
|
-
|
95
|
-
# This is handled automatically by Nokogiri in Ruby 1.9, IF charset of string != utf-8
|
96
|
-
# We want UTF-8 to be specified as well, so we still do this.
|
97
|
-
unless document.at_css('html > head > meta[http-equiv=Content-Type]')
|
98
|
-
meta = Nokogiri::XML::Node.new('meta', document)
|
99
|
-
meta['http-equiv'] = 'Content-Type'
|
100
|
-
meta['content'] = 'text/html; charset=UTF-8'
|
101
|
-
head.add_child(meta)
|
102
|
-
end
|
103
|
-
end
|
49
|
+
style_map
|
50
|
+
end
|
104
51
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
link.remove
|
52
|
+
def each_inlinable_block
|
53
|
+
stylesheets.each do |stylesheet|
|
54
|
+
stylesheet.each_inlinable_block do |selector, properties|
|
55
|
+
yield stylesheet, selector, properties
|
110
56
|
end
|
111
57
|
end
|
58
|
+
end
|
112
59
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
@inline_css << style.content
|
117
|
-
style.remove
|
118
|
-
end
|
119
|
-
end
|
60
|
+
def apply_element_style(element, builder)
|
61
|
+
element["style"] = [builder.attribute_string, element["style"]].compact.join(";")
|
62
|
+
end
|
120
63
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
64
|
+
def elements_matching_selector(stylesheet, selector, dom)
|
65
|
+
dom.css(selector.to_s)
|
66
|
+
# There's no way to get a list of supported pseudo selectors, so we're left
|
67
|
+
# with having to rescue errors.
|
68
|
+
# Pseudo selectors that are known to be bad are skipped automatically but
|
69
|
+
# this will catch the rest.
|
70
|
+
rescue Nokogiri::XML::XPath::SyntaxError, Nokogiri::CSS::SyntaxError => error
|
71
|
+
warn "Roadie cannot use #{selector.inspect} (from \"#{stylesheet.name}\" stylesheet) when inlining stylesheets"
|
72
|
+
[]
|
73
|
+
rescue => error
|
74
|
+
warn "Roadie got error when looking for #{selector.inspect} (from \"#{stylesheet.name}\" stylesheet): #{error}"
|
75
|
+
raise unless error.message.include?('XPath')
|
76
|
+
[]
|
77
|
+
end
|
130
78
|
|
131
|
-
|
132
|
-
|
133
|
-
|
79
|
+
# @api private
|
80
|
+
# StyleMap is a map between a DOM element and {StyleAttributeBuilder}. Basically,
|
81
|
+
# it's an accumulator for properties, scoped on specific elements.
|
82
|
+
class StyleMap
|
83
|
+
def initialize
|
84
|
+
@map = Hash.new { |hash, key|
|
85
|
+
hash[key] = StyleAttributeBuilder.new
|
86
|
+
}
|
134
87
|
end
|
135
88
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
each_element_in_selector(selector) do |element|
|
141
|
-
style_declarations_in_rule_set(selector.specificity, rule_set) do |declaration|
|
142
|
-
element_declarations[element] << declaration
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
89
|
+
def add(elements, new_properties)
|
90
|
+
Array(elements).each do |element|
|
91
|
+
new_properties.each do |property|
|
92
|
+
@map[element] << property
|
146
93
|
end
|
147
94
|
end
|
148
95
|
end
|
149
96
|
|
150
|
-
def
|
151
|
-
|
152
|
-
selector = Selector.new(selector_string)
|
153
|
-
yield selector if selector.inlinable?
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
def each_element_in_selector(selector)
|
158
|
-
document.css(selector.to_s).each do |element|
|
159
|
-
yield element
|
160
|
-
end
|
161
|
-
# There's no way to get a list of supported pseudo rules, so we're left
|
162
|
-
# with having to rescue errors.
|
163
|
-
# Pseudo selectors that are known to be bad are skipped automatically but
|
164
|
-
# this will catch the rest.
|
165
|
-
rescue Nokogiri::XML::XPath::SyntaxError, Nokogiri::CSS::SyntaxError => error
|
166
|
-
warn "Roadie cannot use #{selector.inspect} when inlining stylesheets"
|
167
|
-
rescue => error
|
168
|
-
warn "Roadie got error when looking for #{selector.inspect}: #{error}"
|
169
|
-
raise unless error.message.include?('XPath')
|
170
|
-
end
|
171
|
-
|
172
|
-
def style_declarations_in_rule_set(specificity, rule_set)
|
173
|
-
rule_set.each_declaration do |property, value, important|
|
174
|
-
yield StyleDeclaration.new(property, value, important, specificity)
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def make_image_urls_absolute
|
179
|
-
document.css('img').each do |img|
|
180
|
-
img['src'] = ensure_absolute_url(img['src']) if img['src']
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def make_style_urls_absolute
|
185
|
-
document.css('*[style]').each do |element|
|
186
|
-
styling = element['style']
|
187
|
-
element['style'] = styling.gsub(CSS_URL_REGEXP) { "url(#{$1}#{ensure_absolute_url($2, '/stylesheets')}#{$1})" }
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def ensure_absolute_url(url, base_path = nil)
|
192
|
-
base, uri = absolute_url_base(base_path), URI.parse(url)
|
193
|
-
if uri.relative? and base
|
194
|
-
base.merge(uri).to_s
|
195
|
-
else
|
196
|
-
uri.to_s
|
197
|
-
end
|
198
|
-
rescue URI::InvalidURIError
|
199
|
-
return url
|
200
|
-
end
|
201
|
-
|
202
|
-
def absolute_url_base(base_path)
|
203
|
-
return nil unless url_options
|
204
|
-
port = url_options[:port]
|
205
|
-
scheme = protocol_to_scheme url_options[:protocol]
|
206
|
-
URI::Generic.build({
|
207
|
-
:scheme => scheme,
|
208
|
-
:host => url_options[:host],
|
209
|
-
:port => (port ? port.to_i : nil),
|
210
|
-
:path => base_path
|
211
|
-
})
|
212
|
-
end
|
213
|
-
|
214
|
-
# Strip :// from any protocol, if present
|
215
|
-
def protocol_to_scheme(protocol)
|
216
|
-
return 'http' unless protocol
|
217
|
-
protocol.to_s[/^\w+/]
|
218
|
-
end
|
219
|
-
|
220
|
-
def all_link_elements_with_url
|
221
|
-
document.css("link[rel=stylesheet]").map { |link| [link, URI.parse(link['href'])] }
|
222
|
-
end
|
223
|
-
|
224
|
-
def all_link_elements_to_be_inlined_with_url
|
225
|
-
all_link_elements_with_url.reject do |link, url|
|
226
|
-
absolute_path_url = (url.host or url.path.nil?)
|
227
|
-
blacklisted_element = (link['media'] == 'print' or link['data-immutable'])
|
228
|
-
|
229
|
-
absolute_path_url or blacklisted_element
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
CLEANING_MATCHER = /
|
234
|
-
(^\s* # Beginning-of-lines matches
|
235
|
-
(<!\[CDATA\[)|
|
236
|
-
(<!--+)
|
237
|
-
)|( # End-of-line matches
|
238
|
-
(--+>)|
|
239
|
-
(\]\]>)
|
240
|
-
$)
|
241
|
-
/x.freeze
|
242
|
-
|
243
|
-
def clean_css(css)
|
244
|
-
css.gsub(CLEANING_MATCHER, '')
|
97
|
+
def each_element
|
98
|
+
@map.each_pair { |element, builder| yield element, builder }
|
245
99
|
end
|
100
|
+
end
|
246
101
|
end
|
247
102
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Roadie
|
2
|
+
# @api private
|
3
|
+
# Class that improves the markup of a HTML DOM tree
|
4
|
+
#
|
5
|
+
# This class will improve the following aspects of the DOM:
|
6
|
+
# * A HTML5 doctype will be added if missing, other doctypes will be left as-is.
|
7
|
+
# * Basic HTML elements will be added if missing.
|
8
|
+
# * +<html>+
|
9
|
+
# * +<head>+
|
10
|
+
# * +<body>+
|
11
|
+
# * +<meta>+ declaring charset and content-type (text/html)
|
12
|
+
#
|
13
|
+
# @note Due to a Nokogiri bug, the HTML5 doctype cannot be added under JRuby. No doctype is outputted under JRuby.
|
14
|
+
# See https://github.com/sparklemotion/nokogiri/issues/984
|
15
|
+
class MarkupImprover
|
16
|
+
# The original HTML must also be passed in in order to handle the doctypes
|
17
|
+
# since a +Nokogiri::HTML::Document+ will always have a doctype, no matter if
|
18
|
+
# the original source had it or not. Reading the raw HTML is the only way to
|
19
|
+
# determine if we want to add a HTML5 doctype or not.
|
20
|
+
def initialize(dom, original_html)
|
21
|
+
@dom = dom
|
22
|
+
@html = original_html
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [nil] passed DOM will be mutated
|
26
|
+
def improve
|
27
|
+
ensure_doctype_present
|
28
|
+
head = ensure_head_element_present
|
29
|
+
ensure_declared_charset head
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
attr_reader :dom
|
34
|
+
|
35
|
+
def ensure_doctype_present
|
36
|
+
return if uses_buggy_jruby?
|
37
|
+
return if @html.include?('<!DOCTYPE ')
|
38
|
+
# Nokogiri adds a "default" doctype to the DOM, which we will remove
|
39
|
+
dom.internal_subset.remove unless dom.internal_subset.nil?
|
40
|
+
dom.create_internal_subset 'html', nil, nil
|
41
|
+
end
|
42
|
+
|
43
|
+
# JRuby up to at least 1.6.0 has a bug where the doctype of a document cannot be changed.
|
44
|
+
# See https://github.com/sparklemotion/nokogiri/issues/984
|
45
|
+
def uses_buggy_jruby?
|
46
|
+
# No reason to check for version yet since no existing version has a fix.
|
47
|
+
defined?(JRuby)
|
48
|
+
end
|
49
|
+
|
50
|
+
def ensure_head_element_present
|
51
|
+
if (head = dom.at_xpath('html/head'))
|
52
|
+
head
|
53
|
+
else
|
54
|
+
create_head_element dom.at_xpath('html')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_head_element(parent)
|
59
|
+
head = Nokogiri::XML::Node.new 'head', dom
|
60
|
+
unless parent.children.empty?
|
61
|
+
# Crashes when no children are present
|
62
|
+
parent.children.before head
|
63
|
+
else
|
64
|
+
parent << head
|
65
|
+
end
|
66
|
+
head
|
67
|
+
end
|
68
|
+
|
69
|
+
def ensure_declared_charset(parent)
|
70
|
+
if content_type_meta_element_missing?
|
71
|
+
parent.add_child make_content_type_element
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def content_type_meta_element_missing?
|
76
|
+
dom.xpath('html/head/meta').none? do |meta|
|
77
|
+
meta['http-equiv'].to_s.downcase == 'content-type'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def make_content_type_element
|
82
|
+
meta = Nokogiri::XML::Node.new('meta', dom)
|
83
|
+
meta['http-equiv'] = 'Content-Type'
|
84
|
+
meta['content'] = 'text/html; charset=UTF-8'
|
85
|
+
meta
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|