roadie 2.4.3 → 3.0.0.pre1
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 +9 -14
- data/.yardopts +1 -1
- data/Changelog.md +22 -10
- data/Gemfile +3 -0
- data/Guardfile +11 -1
- data/README.md +165 -163
- data/Rakefile +2 -19
- data/lib/roadie.rb +14 -69
- 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 +21 -62
- data/lib/roadie/inliner.rb +71 -218
- 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 +67 -0
- data/lib/roadie/rspec.rb +1 -0
- data/lib/roadie/rspec/asset_provider.rb +49 -0
- data/lib/roadie/selector.rb +42 -18
- data/lib/roadie/style_block.rb +33 -0
- data/lib/roadie/style_properties.rb +29 -0
- data/lib/roadie/style_property.rb +93 -0
- data/lib/roadie/stylesheet.rb +65 -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 +6 -10
- 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 +16 -0
- data/spec/lib/roadie/document_spec.rb +123 -0
- data/spec/lib/roadie/filesystem_provider_spec.rb +25 -72
- 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 +81 -0
- data/spec/lib/roadie/selector_spec.rb +7 -5
- data/spec/lib/roadie/style_block_spec.rb +35 -0
- data/spec/lib/roadie/style_properties_spec.rb +61 -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 +120 -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 +5 -60
- data/spec/support/have_node_matcher.rb +2 -2
- data/spec/support/have_selector_matcher.rb +1 -1
- data/spec/support/have_styling_matcher.rb +48 -14
- data/spec/support/test_provider.rb +13 -0
- metadata +73 -177
- 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,33 @@
|
|
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
|
-
# 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
|
14
|
+
def initialize(path = Dir.pwd)
|
15
|
+
@path = path
|
39
16
|
end
|
40
17
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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)
|
18
|
+
# @raise InsecurePathError
|
19
|
+
# @return [Stylesheet, nil]
|
20
|
+
def find_stylesheet(name)
|
21
|
+
file_path = build_file_path(name)
|
22
|
+
if File.exist? file_path
|
23
|
+
Stylesheet.new file_path, File.read(file_path)
|
53
24
|
end
|
54
25
|
end
|
55
26
|
|
56
27
|
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
|
28
|
+
def build_file_path(name)
|
29
|
+
raise InsecurePathError, name if name.include?("..")
|
30
|
+
File.join(@path, name)
|
31
|
+
end
|
73
32
|
end
|
74
33
|
end
|
data/lib/roadie/inliner.rb
CHANGED
@@ -1,247 +1,100 @@
|
|
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
|
-
|
66
|
-
|
67
|
-
def inline_css
|
68
|
-
@inline_css.join("\n")
|
69
|
-
end
|
70
|
-
|
71
|
-
def parsed_css
|
72
|
-
CssParser::Parser.new.tap do |parser|
|
73
|
-
parser.add_block! clean_css(css) if css
|
74
|
-
parser.add_block! clean_css(inline_css)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def adjust_html
|
79
|
-
Nokogiri::HTML.parse(html).tap do |document|
|
80
|
-
yield document
|
81
|
-
end.dup.to_html
|
82
|
-
end
|
83
|
-
|
84
|
-
def add_missing_structure
|
85
|
-
html_node = document.at_css('html')
|
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
|
104
|
-
|
105
|
-
def extract_link_elements
|
106
|
-
all_link_elements_to_be_inlined_with_url.each do |link, url|
|
107
|
-
asset = assets.find(url.path)
|
108
|
-
@inline_css << asset.to_s
|
109
|
-
link.remove
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def extract_inline_style_elements
|
114
|
-
document.css("style").each do |style|
|
115
|
-
next if style['media'] == 'print' or style['data-immutable']
|
116
|
-
@inline_css << style.content
|
117
|
-
style.remove
|
118
|
-
end
|
119
|
-
end
|
33
|
+
attr_reader :stylesheets
|
120
34
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
seen_properties = Set.new
|
125
|
-
declarations.sort.reverse_each do |declaration|
|
126
|
-
next if seen_properties.include?(declaration.property)
|
127
|
-
ordered_declarations.unshift(declaration)
|
128
|
-
seen_properties << declaration.property
|
129
|
-
end
|
130
|
-
|
131
|
-
rules_string = ordered_declarations.map { |declaration| declaration.to_s }.join(';')
|
132
|
-
element['style'] = [rules_string, element['style']].compact.join(';')
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def elements_with_declarations
|
137
|
-
Hash.new { |hash, key| hash[key] = [] }.tap do |element_declarations|
|
138
|
-
parsed_css.each_rule_set do |rule_set, _|
|
139
|
-
each_good_selector(rule_set) do |selector|
|
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
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def each_good_selector(rules)
|
151
|
-
rules.selectors.each do |selector_string|
|
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')
|
35
|
+
def apply(style_map)
|
36
|
+
style_map.each_element do |element, properties|
|
37
|
+
apply_element_style element, properties
|
170
38
|
end
|
39
|
+
end
|
171
40
|
|
172
|
-
|
173
|
-
|
174
|
-
yield StyleDeclaration.new(property, value, important, specificity)
|
175
|
-
end
|
176
|
-
end
|
41
|
+
def style_map(dom)
|
42
|
+
style_map = StyleMap.new
|
177
43
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
end
|
44
|
+
each_inlinable_block do |stylesheet, selector, properties|
|
45
|
+
elements = elements_matching_selector(stylesheet, selector, dom)
|
46
|
+
style_map.add elements, properties
|
182
47
|
end
|
183
48
|
|
184
|
-
|
185
|
-
|
186
|
-
styling = element['style']
|
187
|
-
element['style'] = styling.gsub(CSS_URL_REGEXP) { "url(#{$1}#{ensure_absolute_url($2, '/stylesheets')}#{$1})" }
|
188
|
-
end
|
189
|
-
end
|
49
|
+
style_map
|
50
|
+
end
|
190
51
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
else
|
196
|
-
uri.to_s
|
52
|
+
def each_inlinable_block
|
53
|
+
stylesheets.each do |stylesheet|
|
54
|
+
stylesheet.each_inlinable_block do |selector, properties|
|
55
|
+
yield stylesheet, selector, properties
|
197
56
|
end
|
198
|
-
rescue URI::InvalidURIError
|
199
|
-
return url
|
200
57
|
end
|
58
|
+
end
|
201
59
|
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
60
|
+
def apply_element_style(element, properties)
|
61
|
+
element["style"] = [properties.to_s, element["style"]].compact.join(";")
|
62
|
+
end
|
213
63
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
219
78
|
|
220
|
-
|
221
|
-
|
79
|
+
# @api private
|
80
|
+
# StyleMap is a map between a DOM element and {StyleProperties}. 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] = StyleProperties.new([])
|
86
|
+
}
|
222
87
|
end
|
223
88
|
|
224
|
-
def
|
225
|
-
|
226
|
-
|
227
|
-
blacklisted_element = (link['media'] == 'print' or link['data-immutable'])
|
228
|
-
|
229
|
-
absolute_path_url or blacklisted_element
|
89
|
+
def add(elements, new_properties)
|
90
|
+
Array(elements).each do |element|
|
91
|
+
@map[element].merge!(new_properties)
|
230
92
|
end
|
231
93
|
end
|
232
94
|
|
233
|
-
|
234
|
-
|
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, '')
|
95
|
+
def each_element
|
96
|
+
@map.each_pair { |element, properties| yield element, properties }
|
245
97
|
end
|
98
|
+
end
|
246
99
|
end
|
247
100
|
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'].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
|