nokogiri 1.6.1 → 1.6.2.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of nokogiri might be problematic. Click here for more details.

Files changed (93) hide show
  1. checksums.yaml +7 -7
  2. data/.editorconfig +17 -0
  3. data/.travis.yml +4 -6
  4. data/CHANGELOG.ja.rdoc +37 -8
  5. data/CHANGELOG.rdoc +48 -3
  6. data/Gemfile +3 -3
  7. data/Manifest.txt +57 -1
  8. data/README.ja.rdoc +22 -16
  9. data/README.rdoc +24 -19
  10. data/ROADMAP.md +1 -2
  11. data/Rakefile +161 -58
  12. data/build_all +56 -31
  13. data/dependencies.yml +3 -3
  14. data/ext/nokogiri/extconf.rb +379 -121
  15. data/ext/nokogiri/html_document.c +2 -2
  16. data/ext/nokogiri/nokogiri.c +6 -1
  17. data/ext/nokogiri/xml_document.c +5 -4
  18. data/ext/nokogiri/xml_node.c +11 -4
  19. data/ext/nokogiri/xml_reader.c +1 -1
  20. data/ext/nokogiri/xml_sax_parser_context.c +40 -0
  21. data/ext/nokogiri/xml_syntax_error.c +10 -5
  22. data/ext/nokogiri/xml_syntax_error.h +1 -1
  23. data/ext/nokogiri/xml_xpath_context.c +2 -14
  24. data/ext/nokogiri/xslt_stylesheet.c +1 -1
  25. data/lib/nokogiri.rb +31 -22
  26. data/lib/nokogiri/css/node.rb +0 -50
  27. data/lib/nokogiri/css/parser.rb +213 -218
  28. data/lib/nokogiri/css/parser.y +21 -30
  29. data/lib/nokogiri/css/xpath_visitor.rb +62 -14
  30. data/lib/nokogiri/html/document.rb +97 -18
  31. data/lib/nokogiri/html/sax/parser.rb +2 -2
  32. data/lib/nokogiri/version.rb +1 -1
  33. data/lib/nokogiri/xml/builder.rb +1 -1
  34. data/lib/nokogiri/xml/document.rb +2 -2
  35. data/lib/nokogiri/xml/dtd.rb +10 -0
  36. data/lib/nokogiri/xml/node.rb +26 -1
  37. data/lib/nokogiri/xml/sax/parser.rb +1 -1
  38. data/ports/archives/libxslt-1.1.28.tar.gz +0 -0
  39. data/ports/patches/libxml2/0001-Fix-parser-local-buffers-size-problems.patch +265 -0
  40. data/ports/patches/libxml2/0002-Fix-entities-local-buffers-size-problems.patch +102 -0
  41. data/ports/patches/libxml2/0003-Fix-an-error-in-previous-commit.patch +26 -0
  42. data/ports/patches/libxml2/0004-Fix-potential-out-of-bound-access.patch +26 -0
  43. data/ports/patches/libxml2/0005-Detect-excessive-entities-expansion-upon-replacement.patch +158 -0
  44. data/ports/patches/libxml2/0006-Do-not-fetch-external-parsed-entities.patch +78 -0
  45. data/ports/patches/libxml2/0007-Enforce-XML_PARSER_EOF-state-handling-through-the-pa.patch +480 -0
  46. data/ports/patches/libxml2/0008-Improve-handling-of-xmlStopParser.patch +315 -0
  47. data/ports/patches/libxml2/0009-Fix-a-couple-of-return-without-value.patch +37 -0
  48. data/ports/patches/libxslt/0001-Adding-doc-update-related-to-1.1.28.patch +222 -0
  49. data/ports/patches/libxslt/0002-Fix-a-couple-of-places-where-f-printf-parameters-wer.patch +53 -0
  50. data/ports/patches/libxslt/0003-Initialize-pseudo-random-number-generator-with-curre.patch +60 -0
  51. data/ports/patches/libxslt/0004-EXSLT-function-str-replace-is-broken-as-is.patch +42 -0
  52. data/ports/patches/libxslt/0006-Fix-str-padding-to-work-with-UTF-8-strings.patch +164 -0
  53. data/ports/patches/libxslt/0007-Separate-function-for-predicate-matching-in-patterns.patch +587 -0
  54. data/ports/patches/libxslt/0008-Fix-direct-pattern-matching.patch +80 -0
  55. data/ports/patches/libxslt/0009-Fix-certain-patterns-with-predicates.patch +185 -0
  56. data/ports/patches/libxslt/0010-Fix-handling-of-UTF-8-strings-in-EXSLT-crypto-module.patch +126 -0
  57. data/ports/patches/libxslt/0013-Memory-leak-in-xsltCompileIdKeyPattern-error-path.patch +25 -0
  58. data/ports/patches/libxslt/0014-Fix-for-bug-436589.patch +43 -0
  59. data/ports/patches/libxslt/0015-Fix-mkdir-for-mingw.patch +41 -0
  60. data/suppressions/README.txt +1 -0
  61. data/suppressions/nokogiri_ree-1.8.7.358.supp +61 -0
  62. data/suppressions/nokogiri_ruby-1.8.7.370.supp +0 -0
  63. data/suppressions/nokogiri_ruby-1.9.2.320.supp +28 -0
  64. data/suppressions/nokogiri_ruby-1.9.3.327.supp +28 -0
  65. data/test/css/test_nthiness.rb +65 -2
  66. data/test/css/test_parser.rb +27 -10
  67. data/test/css/test_tokenizer.rb +1 -1
  68. data/test/css/test_xpath_visitor.rb +6 -1
  69. data/test/files/atom.xml +344 -0
  70. data/test/files/shift_jis_no_charset.html +9 -0
  71. data/test/helper.rb +10 -0
  72. data/test/html/test_document.rb +74 -7
  73. data/test/html/test_document_encoding.rb +10 -0
  74. data/test/html/test_document_fragment.rb +3 -3
  75. data/test/namespaces/test_namespaces_in_cloned_doc.rb +31 -0
  76. data/test/test_nokogiri.rb +6 -0
  77. data/test/test_reader.rb +7 -4
  78. data/test/test_xslt_transforms.rb +25 -0
  79. data/test/xml/sax/test_parser.rb +16 -0
  80. data/test/xml/sax/test_parser_context.rb +9 -0
  81. data/test/xml/test_builder.rb +9 -0
  82. data/test/xml/test_c14n.rb +12 -2
  83. data/test/xml/test_document.rb +66 -0
  84. data/test/xml/test_document_fragment.rb +5 -0
  85. data/test/xml/test_dtd.rb +84 -0
  86. data/test/xml/test_entity_reference.rb +3 -3
  87. data/test/xml/test_node.rb +21 -3
  88. data/test/xml/test_node_attributes.rb +17 -0
  89. data/test/xml/test_schema.rb +26 -0
  90. data/test/xml/test_xpath.rb +81 -0
  91. metadata +254 -174
  92. data/ports/archives/libxslt-1.1.26.tar.gz +0 -0
  93. data/tasks/cross_compile.rb +0 -134
@@ -28,17 +28,6 @@ rule
28
28
  Node.new(:CONDITIONAL_SELECTOR, [val.first, val[1]])
29
29
  end
30
30
  }
31
- | element_name hcap_1toN negation {
32
- result = Node.new(:CONDITIONAL_SELECTOR,
33
- [
34
- val.first,
35
- Node.new(:COMBINATOR, [val[1], val.last])
36
- ]
37
- )
38
- }
39
- | element_name negation {
40
- result = Node.new(:CONDITIONAL_SELECTOR, val)
41
- }
42
31
  | function
43
32
  | function pseudo {
44
33
  result = Node.new(:CONDITIONAL_SELECTOR, val)
@@ -46,14 +35,6 @@ rule
46
35
  | function attrib {
47
36
  result = Node.new(:CONDITIONAL_SELECTOR, val)
48
37
  }
49
- | hcap_1toN negation {
50
- result = Node.new(:CONDITIONAL_SELECTOR,
51
- [
52
- Node.new(:ELEMENT_NAME, ['*']),
53
- Node.new(:COMBINATOR, val)
54
- ]
55
- )
56
- }
57
38
  | hcap_1toN {
58
39
  result = Node.new(:CONDITIONAL_SELECTOR,
59
40
  [Node.new(:ELEMENT_NAME, ['*']), val.first]
@@ -130,7 +111,7 @@ rule
130
111
  | FUNCTION expr RPAREN {
131
112
  result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
132
113
  }
133
- | FUNCTION an_plus_b RPAREN {
114
+ | FUNCTION nth RPAREN {
134
115
  result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
135
116
  }
136
117
  | NOT expr RPAREN {
@@ -150,10 +131,10 @@ rule
150
131
  {
151
132
  if val[0] == 'even'
152
133
  val = ["2","n","+","0"]
153
- result = Node.new(:AN_PLUS_B, val)
134
+ result = Node.new(:NTH, val)
154
135
  elsif val[0] == 'odd'
155
136
  val = ["2","n","+","1"]
156
- result = Node.new(:AN_PLUS_B, val)
137
+ result = Node.new(:NTH, val)
157
138
  else
158
139
  # This is not CSS standard. It allows us to support this:
159
140
  # assert_xpath("//a[foo(., @href)]", @parser.parse('a:foo(@href)'))
@@ -163,11 +144,11 @@ rule
163
144
  end
164
145
  }
165
146
  ;
166
- an_plus_b
147
+ nth
167
148
  : NUMBER IDENT PLUS NUMBER # 5n+3 -5n+3
168
149
  {
169
150
  if val[1] == 'n'
170
- result = Node.new(:AN_PLUS_B, val)
151
+ result = Node.new(:NTH, val)
171
152
  else
172
153
  raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
173
154
  end
@@ -175,21 +156,27 @@ rule
175
156
  | IDENT PLUS NUMBER { # n+3, -n+3
176
157
  if val[0] == 'n'
177
158
  val.unshift("1")
178
- result = Node.new(:AN_PLUS_B, val)
159
+ result = Node.new(:NTH, val)
179
160
  elsif val[0] == '-n'
180
161
  val[0] = 'n'
181
162
  val.unshift("-1")
182
- result = Node.new(:AN_PLUS_B, val)
163
+ result = Node.new(:NTH, val)
183
164
  else
184
165
  raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
185
166
  end
186
167
  }
187
- | NUMBER IDENT # 5n, -5n
188
- {
189
- if val[1] == 'n'
168
+ | NUMBER IDENT { # 5n, -5n, 10n-1
169
+ n = val[1]
170
+ if n[0, 2] == 'n-'
171
+ val[1] = 'n'
172
+ val << "-"
173
+ # b is contained in n as n is the string "n-b"
174
+ val << n[2, n.size]
175
+ result = Node.new(:NTH, val)
176
+ elsif n == 'n'
190
177
  val << "+"
191
178
  val << "0"
192
- result = Node.new(:AN_PLUS_B, val)
179
+ result = Node.new(:NTH, val)
193
180
  else
194
181
  raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
195
182
  end
@@ -218,10 +205,14 @@ rule
218
205
  | pseudo hcap_1toN {
219
206
  result = Node.new(:COMBINATOR, val)
220
207
  }
208
+ | negation hcap_1toN {
209
+ result = Node.new(:COMBINATOR, val)
210
+ }
221
211
  | attribute_id
222
212
  | class
223
213
  | attrib
224
214
  | pseudo
215
+ | negation
225
216
  ;
226
217
  attribute_id
227
218
  : HASH { result = Node.new(:ID, val) }
@@ -2,7 +2,7 @@ module Nokogiri
2
2
  module CSS
3
3
  class XPathVisitor # :nodoc:
4
4
  def visit_function node
5
- # note that nth-child and nth-last-child are preprocessed in css/node.rb.
5
+
6
6
  msg = :"visit_function_#{node.value.first.gsub(/[(]/, '')}"
7
7
  return self.send(msg, node) if self.respond_to?(msg)
8
8
 
@@ -13,19 +13,31 @@ module Nokogiri
13
13
  "self::#{node.value[1]}"
14
14
  when /^eq\(/
15
15
  "position() = #{node.value[1]}"
16
- when /^(nth|nth-of-type|nth-child)\(/
17
- if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :AN_PLUS_B
18
- an_plus_b(node.value[1])
16
+ when /^(nth|nth-of-type)\(/
17
+ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
18
+ nth(node.value[1])
19
19
  else
20
20
  "position() = #{node.value[1]}"
21
21
  end
22
- when /^(nth-last-child|nth-last-of-type)\(/
23
- if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :AN_PLUS_B
24
- an_plus_b(node.value[1], :last => true)
22
+ when /^nth-child\(/
23
+ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
24
+ nth(node.value[1], :child => true)
25
+ else
26
+ "count(preceding-sibling::*) = #{node.value[1].to_i-1}"
27
+ end
28
+ when /^nth-last-of-type\(/
29
+ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
30
+ nth(node.value[1], :last => true)
25
31
  else
26
32
  index = node.value[1].to_i - 1
27
33
  index == 0 ? "position() = last()" : "position() = last() - #{index}"
28
34
  end
35
+ when /^nth-last-child\(/
36
+ if node.value[1].is_a?(Nokogiri::CSS::Node) and node.value[1].type == :NTH
37
+ nth(node.value[1], :last => true, :child => true)
38
+ else
39
+ "count(following-sbiling::*) = #{node.value[1].to_i-1}"
40
+ end
29
41
  when /^(first|first-of-type)\(/
30
42
  "position() = 1"
31
43
  when /^(last|last-of-type)\(/
@@ -105,10 +117,13 @@ module Nokogiri
105
117
  return self.send(msg, node) if self.respond_to?(msg)
106
118
 
107
119
  case node.value.first
108
- when "first", "first-child" then "position() = 1"
109
- when "last", "last-child" then "position() = last()"
120
+ when "first" then "position() = 1"
121
+ when "first-child" then "count(preceding-sibling::*) = 0"
122
+ when "last" then "position() = last()"
123
+ when "last-child" then "count(following-sibling::*) = 0"
110
124
  when "first-of-type" then "position() = 1"
111
125
  when "last-of-type" then "position() = last()"
126
+ when "only-child" then "count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0"
112
127
  when "only-of-type" then "last() = 1"
113
128
  when "empty" then "not(node())"
114
129
  when "parent" then "node()"
@@ -123,8 +138,15 @@ module Nokogiri
123
138
  "contains(concat(' ', normalize-space(@class), ' '), ' #{node.value.first} ')"
124
139
  end
125
140
 
141
+ def visit_combinator node
142
+ if is_of_type_pseudo_class?(node.value.last)
143
+ "#{node.value.first.accept(self) if node.value.first}][#{node.value.last.accept(self)}"
144
+ else
145
+ "#{node.value.first.accept(self) if node.value.first} and #{node.value.last.accept(self)}"
146
+ end
147
+ end
148
+
126
149
  {
127
- 'combinator' => ' and ',
128
150
  'direct_adjacent_selector' => "/following-sibling::*[1]/self::",
129
151
  'following_selector' => "/following-sibling::",
130
152
  'descendant_selector' => '//',
@@ -151,12 +173,15 @@ module Nokogiri
151
173
  end
152
174
 
153
175
  private
154
- def an_plus_b node, options={}
176
+ def nth node, options={}
155
177
  raise ArgumentError, "expected an+b node to contain 4 tokens, but is #{node.value.inspect}" unless node.value.size == 4
156
178
 
157
- a = node.value[0].to_i
158
- b = node.value[3].to_i
159
- position = options[:last] ? "(last()-position()+1)" : "position()"
179
+ a, b = read_a_and_positive_b node.value
180
+ position = if options[:child]
181
+ options[:last] ? "(count(following-sibling::*) + 1)" : "(count(preceding-sibling::*) + 1)"
182
+ else
183
+ options[:last] ? "(last()-position()+1)" : "position()"
184
+ end
160
185
 
161
186
  if (b == 0)
162
187
  return "(#{position} mod #{a}) = 0"
@@ -166,6 +191,29 @@ module Nokogiri
166
191
  end
167
192
  end
168
193
 
194
+ def read_a_and_positive_b values
195
+ op = values[2]
196
+ if op == "+"
197
+ a = values[0].to_i
198
+ b = values[3].to_i
199
+ elsif op == "-"
200
+ a = values[0].to_i
201
+ b = a - (values[3].to_i % a)
202
+ else
203
+ raise ArgumentError, "expected an+b node to have either + or - as the operator, but is #{op.inspect}"
204
+ end
205
+ [a, b]
206
+ end
207
+
208
+ def is_of_type_pseudo_class? node
209
+ if node.type==:PSEUDO_CLASS
210
+ if node.value[0].is_a?(Nokogiri::CSS::Node) and node.value[0].type == :FUNCTION
211
+ node.value[0].value[0]
212
+ else
213
+ node.value[0]
214
+ end =~ /(nth|first|last|only)-of-type(\()?/
215
+ end
216
+ end
169
217
  end
170
218
  end
171
219
  end
@@ -5,24 +5,59 @@ module Nokogiri
5
5
  # Get the meta tag encoding for this document. If there is no meta tag,
6
6
  # then nil is returned.
7
7
  def meta_encoding
8
- meta = meta_content_type and
9
- match = /charset\s*=\s*([\w-]+)/i.match(meta['content']) and
10
- match[1]
8
+ case
9
+ when meta = at('//meta[@charset]')
10
+ meta[:charset]
11
+ when meta = meta_content_type
12
+ meta['content'][/charset\s*=\s*([\w-]+)/i, 1]
13
+ end
11
14
  end
12
15
 
13
16
  ###
14
- # Set the meta tag encoding for this document. If there is no meta
15
- # content tag, the encoding is not set.
17
+ # Set the meta tag encoding for this document.
18
+ #
19
+ # If an meta encoding tag is already present, its content is
20
+ # replaced with the given text.
21
+ #
22
+ # Otherwise, this method tries to create one at an appropriate
23
+ # place supplying head and/or html elements as necessary, which
24
+ # is inside a head element if any, and before any text node or
25
+ # content element (typically <body>) if any.
26
+ #
27
+ # The result when trying to set an encoding that is different
28
+ # from the document encoding is undefined.
29
+ #
30
+ # Beware in CRuby, that libxml2 automatically inserts a meta tag
31
+ # into a head element.
16
32
  def meta_encoding= encoding
17
- meta = meta_content_type and
18
- meta['content'] = "text/html; charset=%s" % encoding
33
+ case
34
+ when meta = meta_content_type
35
+ meta['content'] = 'text/html; charset=%s' % encoding
36
+ encoding
37
+ when meta = at('//meta[@charset]')
38
+ meta['charset'] = encoding
39
+ else
40
+ meta = XML::Node.new('meta', self)
41
+ if dtd = internal_subset and dtd.html5_dtd?
42
+ meta['charset'] = encoding
43
+ else
44
+ meta['http-equiv'] = 'Content-Type'
45
+ meta['content'] = 'text/html; charset=%s' % encoding
46
+ end
47
+
48
+ case
49
+ when head = at('//head')
50
+ head.prepend_child(meta)
51
+ else
52
+ set_metadata_element(meta)
53
+ end
54
+ encoding
55
+ end
19
56
  end
20
57
 
21
58
  def meta_content_type
22
- css('meta[@http-equiv]').find { |node|
23
- node['http-equiv'] =~ /\AContent-Type\z/i and
24
- !node['content'].nil? and
25
- !node['content'].empty?
59
+ xpath('//meta[@http-equiv and boolean(@content)]').find { |node|
60
+ node['http-equiv'] =~ /\AContent-Type\z/i
26
61
  }
27
62
  end
28
63
  private :meta_content_type
@@ -31,20 +66,64 @@ module Nokogiri
31
66
  # Get the title string of this document. Return nil if there is
32
67
  # no title tag.
33
68
  def title
34
- title = at('title') and title.inner_text
69
+ title = at('//title') and title.inner_text
35
70
  end
36
71
 
37
72
  ###
38
- # Set the title string of this document. If there is no head
39
- # element, the title is not set.
73
+ # Set the title string of this document.
74
+ #
75
+ # If a title element is already present, its content is replaced
76
+ # with the given text.
77
+ #
78
+ # Otherwise, this method tries to create one at an appropriate
79
+ # place supplying head and/or html elements as necessary, which
80
+ # is inside a head element if any, right after a meta
81
+ # encoding/charset tag if any, and before any text node or
82
+ # content element (typically <body>) if any.
40
83
  def title=(text)
41
- unless title = at('title')
42
- head = at('head') or return nil
43
- title = Nokogiri::XML::Node.new('title', self)
84
+ tnode = XML::Text.new(text, self)
85
+ if title = at('//title')
86
+ title.children = tnode
87
+ return text
88
+ end
89
+
90
+ title = XML::Node.new('title', self) << tnode
91
+ case
92
+ when head = at('//head')
44
93
  head << title
94
+ when meta = at('//meta[@charset]') || meta_content_type
95
+ # better put after charset declaration
96
+ meta.add_next_sibling(title)
97
+ else
98
+ set_metadata_element(title)
99
+ end
100
+ text
101
+ end
102
+
103
+ def set_metadata_element(element)
104
+ case
105
+ when head = at('//head')
106
+ head << element
107
+ when html = at('//html')
108
+ head = html.prepend_child(XML::Node.new('head', self))
109
+ head.prepend_child(element)
110
+ when first = children.find { |node|
111
+ case node
112
+ when XML::Element, XML::Text
113
+ true
114
+ end
115
+ }
116
+ # We reach here only if the underlying document model
117
+ # allows <html>/<head> elements to be omitted and does not
118
+ # automatically supply them.
119
+ first.add_previous_sibling(element)
120
+ else
121
+ html = add_child(XML::Node.new('html', self))
122
+ head = html.add_child(XML::Node.new('head', self))
123
+ head.prepend_child(element)
45
124
  end
46
- title.children = XML::Text.new(text, self)
47
125
  end
126
+ private :set_metadata_element
48
127
 
49
128
  ####
50
129
  # Serialize Node using +options+. Save options can also be set using a
@@ -22,7 +22,7 @@ module Nokogiri
22
22
  # end
23
23
  #
24
24
  # parser = Nokogiri::HTML::SAX::Parser.new(MyDoc.new)
25
- # parser.parse(File.read(ARGV[0], 'rb'))
25
+ # parser.parse(File.read(ARGV[0], mode: 'rb'))
26
26
  #
27
27
  # For more information on SAX parsers, see Nokogiri::XML::SAX
28
28
  class Parser < Nokogiri::XML::SAX::Parser
@@ -40,7 +40,7 @@ module Nokogiri
40
40
  # Parse a file with +filename+
41
41
  def parse_file filename, encoding = 'UTF-8'
42
42
  raise ArgumentError unless filename
43
- raise Errno::ENOENT unless File.exists?(filename)
43
+ raise Errno::ENOENT unless File.exist?(filename)
44
44
  raise Errno::EISDIR if File.directory?(filename)
45
45
  ctx = ParserContext.file(filename, encoding)
46
46
  yield ctx if block_given?
@@ -1,6 +1,6 @@
1
1
  module Nokogiri
2
2
  # The version of Nokogiri you are using
3
- VERSION = '1.6.1'
3
+ VERSION = '1.6.2.rc1'
4
4
 
5
5
  class VersionInfo # :nodoc:
6
6
  def jruby?
@@ -380,7 +380,7 @@ module Nokogiri
380
380
  ###
381
381
  # Insert +node+ as a child of the current Node
382
382
  def insert(node, &block)
383
- node.parent = @parent
383
+ node = @parent.add_child(node)
384
384
  if block_given?
385
385
  old_parent = @parent
386
386
  @parent = node
@@ -45,7 +45,7 @@ module Nokogiri
45
45
  # Give the options to the user
46
46
  yield options if block_given?
47
47
 
48
- return new if empty_doc?(string_or_io)
48
+ return new if !options.strict? && empty_doc?(string_or_io)
49
49
 
50
50
  doc = if string_or_io.respond_to?(:read)
51
51
  url ||= string_or_io.respond_to?(:path) ? string_or_io.path : nil
@@ -233,7 +233,7 @@ module Nokogiri
233
233
  undef_method :namespace_definitions, :line, :add_namespace
234
234
 
235
235
  def add_child node_or_tags
236
- raise "Document already has a root node" if root
236
+ raise "Document already has a root node" if root && root.name != 'nokogiri_text_wrapper'
237
237
  node_or_tags = coerce(node_or_tags)
238
238
  if node_or_tags.is_a?(XML::NodeSet)
239
239
  raise "Document cannot have multiple root nodes" if node_or_tags.size > 1
@@ -17,6 +17,16 @@ module Nokogiri
17
17
  block.call([key, value])
18
18
  }
19
19
  end
20
+
21
+ def html_dtd?
22
+ name.casecmp('html').zero?
23
+ end
24
+
25
+ def html5_dtd?
26
+ html_dtd? &&
27
+ external_id.nil? &&
28
+ (system_id.nil? || system_id == 'about:legacy-compat')
29
+ end
20
30
  end
21
31
  end
22
32
  end