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.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +10 -14
  4. data/.yardopts +1 -1
  5. data/Changelog.md +38 -5
  6. data/Gemfile +3 -4
  7. data/Guardfile +12 -1
  8. data/README.md +168 -164
  9. data/Rakefile +2 -19
  10. data/lib/roadie.rb +15 -68
  11. data/lib/roadie/asset_provider.rb +7 -58
  12. data/lib/roadie/asset_scanner.rb +92 -0
  13. data/lib/roadie/document.rb +103 -0
  14. data/lib/roadie/errors.rb +57 -0
  15. data/lib/roadie/filesystem_provider.rb +30 -60
  16. data/lib/roadie/inliner.rb +72 -217
  17. data/lib/roadie/markup_improver.rb +88 -0
  18. data/lib/roadie/null_provider.rb +13 -0
  19. data/lib/roadie/null_url_rewriter.rb +12 -0
  20. data/lib/roadie/provider_list.rb +71 -0
  21. data/lib/roadie/rspec.rb +1 -0
  22. data/lib/roadie/rspec/asset_provider.rb +49 -0
  23. data/lib/roadie/selector.rb +43 -18
  24. data/lib/roadie/style_attribute_builder.rb +25 -0
  25. data/lib/roadie/style_block.rb +32 -0
  26. data/lib/roadie/style_property.rb +93 -0
  27. data/lib/roadie/stylesheet.rb +65 -0
  28. data/lib/roadie/upgrade_guide.rb +36 -0
  29. data/lib/roadie/url_generator.rb +126 -0
  30. data/lib/roadie/url_rewriter.rb +84 -0
  31. data/lib/roadie/version.rb +1 -1
  32. data/roadie.gemspec +8 -11
  33. data/spec/fixtures/big_em.css +1 -0
  34. data/spec/fixtures/stylesheets/green.css +1 -0
  35. data/spec/integration_spec.rb +125 -95
  36. data/spec/lib/roadie/asset_scanner_spec.rb +153 -0
  37. data/spec/lib/roadie/css_not_found_spec.rb +17 -0
  38. data/spec/lib/roadie/document_spec.rb +123 -0
  39. data/spec/lib/roadie/filesystem_provider_spec.rb +44 -68
  40. data/spec/lib/roadie/inliner_spec.rb +105 -537
  41. data/spec/lib/roadie/markup_improver_spec.rb +78 -0
  42. data/spec/lib/roadie/null_provider_spec.rb +21 -0
  43. data/spec/lib/roadie/null_url_rewriter_spec.rb +19 -0
  44. data/spec/lib/roadie/provider_list_spec.rb +89 -0
  45. data/spec/lib/roadie/selector_spec.rb +15 -10
  46. data/spec/lib/roadie/style_attribute_builder_spec.rb +29 -0
  47. data/spec/lib/roadie/style_block_spec.rb +35 -0
  48. data/spec/lib/roadie/style_property_spec.rb +82 -0
  49. data/spec/lib/roadie/stylesheet_spec.rb +41 -0
  50. data/spec/lib/roadie/test_provider_spec.rb +29 -0
  51. data/spec/lib/roadie/url_generator_spec.rb +121 -0
  52. data/spec/lib/roadie/url_rewriter_spec.rb +79 -0
  53. data/spec/shared_examples/asset_provider.rb +11 -0
  54. data/spec/shared_examples/url_rewriter.rb +23 -0
  55. data/spec/spec_helper.rb +6 -60
  56. data/spec/support/have_attribute_matcher.rb +2 -2
  57. data/spec/support/have_node_matcher.rb +4 -4
  58. data/spec/support/have_selector_matcher.rb +3 -3
  59. data/spec/support/have_styling_matcher.rb +51 -15
  60. data/spec/support/test_provider.rb +13 -0
  61. metadata +86 -175
  62. data/Appraisals +0 -15
  63. data/gemfiles/rails_3.0.gemfile +0 -7
  64. data/gemfiles/rails_3.0.gemfile.lock +0 -123
  65. data/gemfiles/rails_3.1.gemfile +0 -7
  66. data/gemfiles/rails_3.1.gemfile.lock +0 -126
  67. data/gemfiles/rails_3.2.gemfile +0 -7
  68. data/gemfiles/rails_3.2.gemfile.lock +0 -124
  69. data/gemfiles/rails_4.0.gemfile +0 -7
  70. data/gemfiles/rails_4.0.gemfile.lock +0 -119
  71. data/lib/roadie/action_mailer_extensions.rb +0 -95
  72. data/lib/roadie/asset_pipeline_provider.rb +0 -28
  73. data/lib/roadie/css_file_not_found.rb +0 -22
  74. data/lib/roadie/railtie.rb +0 -39
  75. data/lib/roadie/style_declaration.rb +0 -42
  76. data/spec/fixtures/app/assets/stylesheets/integration.css +0 -10
  77. data/spec/fixtures/public/stylesheets/integration.css +0 -10
  78. data/spec/fixtures/views/integration_mailer/marketing.html.erb +0 -2
  79. data/spec/fixtures/views/integration_mailer/notification.html.erb +0 -8
  80. data/spec/fixtures/views/integration_mailer/notification.text.erb +0 -6
  81. data/spec/lib/roadie/action_mailer_extensions_spec.rb +0 -227
  82. data/spec/lib/roadie/asset_pipeline_provider_spec.rb +0 -65
  83. data/spec/lib/roadie/css_file_not_found_spec.rb +0 -29
  84. data/spec/lib/roadie/style_declaration_spec.rb +0 -49
  85. data/spec/lib/roadie_spec.rb +0 -101
  86. data/spec/shared_examples/asset_provider_examples.rb +0 -11
  87. data/spec/support/anonymous_mailer.rb +0 -21
  88. data/spec/support/change_url_options.rb +0 -5
  89. data/spec/support/parse_styling.rb +0 -25
@@ -1,74 +1,44 @@
1
- require 'pathname'
2
-
3
1
  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}.
2
+ # Asset provider that looks for files on your local filesystem.
10
3
  #
11
- # @see #initialize
12
- class FilesystemProvider < AssetProvider
13
- # @return [Pathname] Pathname representing the directory of the assets
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
- # 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
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
- # 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
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
- return find("#{base}.css") if base.to_s !~ /\.css$/
52
- raise CSSFileNotFound.new(name, base.to_s)
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
- 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
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
@@ -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
- # 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
+ # @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
- # 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)
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 and return the final HTML output
49
- # @return [String]
50
- def execute
51
- adjust_html do |document|
52
- @document = document
53
- add_missing_structure
54
- extract_link_elements
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
- attr_reader :css, :html, :assets, :url_options, :document, :after_inlining_handler
33
+ attr_reader :stylesheets
66
34
 
67
- def inline_css
68
- @inline_css.join("\n")
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
- 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
41
+ def style_map(dom)
42
+ style_map = StyleMap.new
77
43
 
78
- def adjust_html
79
- Nokogiri::HTML.parse(html).tap do |document|
80
- yield document
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
- 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
49
+ style_map
50
+ end
104
51
 
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
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
- 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
60
+ def apply_element_style(element, builder)
61
+ element["style"] = [builder.attribute_string, element["style"]].compact.join(";")
62
+ end
120
63
 
121
- def inline_css_rules
122
- elements_with_declarations.each do |element, declarations|
123
- ordered_declarations = []
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
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
- rules_string = ordered_declarations.map { |declaration| declaration.to_s }.join(';')
132
- element['style'] = [rules_string, element['style']].compact.join(';')
133
- end
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 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
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 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')
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