roadie 2.4.3 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|