nokogiri 1.8.2 → 1.8.3

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +14 -14
  3. data/CHANGELOG.md +43 -1
  4. data/LICENSE.md +2 -1
  5. data/Manifest.txt +3 -0
  6. data/README.md +20 -21
  7. data/Rakefile +2 -8
  8. data/SECURITY.md +19 -0
  9. data/build_all +2 -2
  10. data/dependencies.yml +11 -11
  11. data/ext/nokogiri/extconf.rb +1 -1
  12. data/ext/nokogiri/html_element_description.c +14 -14
  13. data/ext/nokogiri/xml_cdata.c +6 -4
  14. data/ext/nokogiri/xml_document.c +2 -3
  15. data/ext/nokogiri/xml_dtd.c +2 -2
  16. data/ext/nokogiri/xml_io.c +1 -0
  17. data/ext/nokogiri/xml_namespace.c +3 -9
  18. data/ext/nokogiri/xml_namespace.h +2 -0
  19. data/ext/nokogiri/xml_node.c +23 -15
  20. data/ext/nokogiri/xml_node_set.c +5 -4
  21. data/ext/nokogiri/xml_node_set.h +0 -1
  22. data/ext/nokogiri/xslt_stylesheet.c +2 -2
  23. data/lib/nokogiri/css/parser.rb +108 -90
  24. data/lib/nokogiri/css/parser.y +13 -2
  25. data/lib/nokogiri/css/tokenizer.rb +1 -1
  26. data/lib/nokogiri/css/tokenizer.rex +4 -4
  27. data/lib/nokogiri/css/xpath_visitor.rb +10 -3
  28. data/lib/nokogiri/html/document_fragment.rb +11 -1
  29. data/lib/nokogiri/version.rb +1 -1
  30. data/lib/nokogiri/xml/node.rb +58 -0
  31. data/lib/nokogiri/xml/node_set.rb +32 -18
  32. data/patches/libxml2/0001-Revert-Do-not-URI-escape-in-server-side-includes.patch +78 -0
  33. data/ports/archives/libxml2-2.9.8.tar.gz +0 -0
  34. data/test/css/test_nthiness.rb +21 -21
  35. data/test/css/test_parser.rb +17 -0
  36. data/test/html/test_attributes.rb +85 -0
  37. data/test/html/test_document_fragment.rb +7 -1
  38. data/test/test_css_cache.rb +5 -3
  39. data/test/xml/sax/test_parser.rb +9 -1
  40. data/test/xml/sax/test_push_parser.rb +60 -0
  41. data/test/xml/test_cdata.rb +1 -1
  42. data/test/xml/test_document.rb +5 -5
  43. data/test/xml/test_dtd.rb +4 -4
  44. data/test/xml/test_node.rb +89 -6
  45. data/test/xml/test_node_attributes.rb +3 -3
  46. data/test/xml/test_node_reparenting.rb +18 -0
  47. data/test/xml/test_node_set.rb +31 -4
  48. data/test/xml/test_reader.rb +13 -1
  49. data/test/xml/test_syntax_error.rb +3 -3
  50. data/test/xml/test_xpath.rb +8 -0
  51. metadata +25 -4
  52. data/ports/archives/libxml2-2.9.7.tar.gz +0 -0
@@ -221,8 +221,9 @@ rule
221
221
  : HASH { result = Node.new(:ID, [unescape_css_identifier(val.first)]) }
222
222
  ;
223
223
  attrib_val_0or1
224
- : eql_incl_dash IDENT { result = [val.first, val[1]] }
225
- | eql_incl_dash STRING { result = [val.first, val[1]] }
224
+ : eql_incl_dash IDENT { result = [val.first, unescape_css_identifier(val[1])] }
225
+ | eql_incl_dash STRING { result = [val.first, unescape_css_string(val[1])] }
226
+ | eql_incl_dash NUMBER { result = [val.first, val[1]] }
226
227
  |
227
228
  ;
228
229
  eql_incl_dash
@@ -259,3 +260,13 @@ require 'nokogiri/css/parser_extras'
259
260
  def unescape_css_identifier(identifier)
260
261
  identifier.gsub(/\\(?:([^0-9a-fA-F])|([0-9a-fA-F]{1,6})\s?)/){ |m| $1 || [$2.hex].pack('U') }
261
262
  end
263
+
264
+ def unescape_css_string(str)
265
+ str.gsub(/\\(?:([^0-9a-fA-F])|([0-9a-fA-F]{1,6})\s?)/) do |m|
266
+ if $1=="\n"
267
+ ''
268
+ else
269
+ $1 || [$2.hex].pack('U')
270
+ end
271
+ end
272
+ end
@@ -130,7 +130,7 @@ class Tokenizer # :nodoc:
130
130
  when (text = @ss.scan(/[\s]+/))
131
131
  action { [:S, text] }
132
132
 
133
- when (text = @ss.scan(/"([^\n\r\f"]|\n|\r\n|\r|\f|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*"|'([^\n\r\f']|\n|\r\n|\r|\f|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*'/))
133
+ when (text = @ss.scan(/"([^\n\r\f"]|\n|\r\n|\r|\f|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*(?<!\\)(?:\\{2})*"|'([^\n\r\f']|\n|\r\n|\r|\f|[^\0-\177]|\\[0-9A-Fa-f]{1,6}(\r\n|[\s])?|\\[^\n\r\f0-9A-Fa-f])*(?<!\\)(?:\\{2})*'/))
134
134
  action { [:STRING, text] }
135
135
 
136
136
  when (text = @ss.scan(/./))
@@ -14,8 +14,8 @@ macro
14
14
  nmstart [_A-Za-z]|{nonascii}|{escape}
15
15
  ident [-@]?({nmstart})({nmchar})*
16
16
  name ({nmchar})+
17
- string1 "([^\n\r\f"]|{nl}|{nonascii}|{escape})*"
18
- string2 '([^\n\r\f']|{nl}|{nonascii}|{escape})*'
17
+ string1 "([^\n\r\f"]|{nl}|{nonascii}|{escape})*(?<!\\)(?:\\{2})*"
18
+ string2 '([^\n\r\f']|{nl}|{nonascii}|{escape})*(?<!\\)(?:\\{2})*'
19
19
  string {string1}|{string2}
20
20
 
21
21
  rule
@@ -44,9 +44,9 @@ rule
44
44
  {num} { [:NUMBER, text] }
45
45
  {w}\/\/{w} { [:DOUBLESLASH, text] }
46
46
  {w}\/{w} { [:SLASH, text] }
47
-
47
+
48
48
  U\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})? {[:UNICODE_RANGE, text] }
49
-
49
+
50
50
  [\s]+ { [:S, text] }
51
51
  {string} { [:STRING, text] }
52
52
  . { [text, text] }
@@ -88,6 +88,13 @@ module Nokogiri
88
88
  value = node.value.last
89
89
  value = "'#{value}'" if value !~ /^['"]/
90
90
 
91
+ if (value[0]==value[-1]) && %q{"'}.include?(value[0])
92
+ str_value = value[1..-2]
93
+ if str_value.include?(value[0])
94
+ value = 'concat("' + str_value.split('"', -1).join(%q{", '"', "}) + '", "")'
95
+ end
96
+ end
97
+
91
98
  case node.value[1]
92
99
  when :equal
93
100
  attribute + " = " + "#{value}"
@@ -145,7 +152,7 @@ module Nokogiri
145
152
  "#{node.value.first.accept(self) if node.value.first} and #{node.value.last.accept(self)}"
146
153
  end
147
154
  end
148
-
155
+
149
156
  {
150
157
  'direct_adjacent_selector' => "/following-sibling::*[1]/self::",
151
158
  'following_selector' => "/following-sibling::",
@@ -208,7 +215,7 @@ module Nokogiri
208
215
  end
209
216
  [a, b]
210
217
  end
211
-
218
+
212
219
  def is_of_type_pseudo_class? node
213
220
  if node.type==:PSEUDO_CLASS
214
221
  if node.value[0].is_a?(Nokogiri::CSS::Node) and node.value[0].type == :FUNCTION
@@ -216,7 +223,7 @@ module Nokogiri
216
223
  else
217
224
  node.value[0]
218
225
  end =~ /(nth|first|last|only)-of-type(\()?/
219
- end
226
+ end
220
227
  end
221
228
  end
222
229
  end
@@ -6,7 +6,17 @@ module Nokogiri
6
6
  def self.parse tags, encoding = nil
7
7
  doc = HTML::Document.new
8
8
 
9
- encoding ||= tags.respond_to?(:encoding) ? tags.encoding.name : 'UTF-8'
9
+ encoding ||= if tags.respond_to?(:encoding)
10
+ encoding = tags.encoding
11
+ if encoding == ::Encoding::ASCII_8BIT
12
+ 'UTF-8'
13
+ else
14
+ encoding.name
15
+ end
16
+ else
17
+ 'UTF-8'
18
+ end
19
+
10
20
  doc.encoding = encoding
11
21
 
12
22
  new(doc, tags)
@@ -1,6 +1,6 @@
1
1
  module Nokogiri
2
2
  # The version of Nokogiri you are using
3
- VERSION = '1.8.2'
3
+ VERSION = '1.8.3'
4
4
 
5
5
  class VersionInfo # :nodoc:
6
6
  def jruby?
@@ -350,6 +350,64 @@ module Nokogiri
350
350
  }
351
351
  end
352
352
 
353
+ ###
354
+ # Get the list of class names of this Node, without
355
+ # deduplication or sorting.
356
+ def classes
357
+ self['class'].to_s.scan(/\S+/)
358
+ end
359
+
360
+ ###
361
+ # Add +name+ to the "class" attribute value of this Node and
362
+ # return self. If the value is already in the current value, it
363
+ # is not added. If no "class" attribute exists yet, one is
364
+ # created with the given value.
365
+ #
366
+ # More than one class may be added at a time, separated by a
367
+ # space.
368
+ def add_class name
369
+ names = classes
370
+ self['class'] = (names + (name.scan(/\S+/) - names)).join(' ')
371
+ self
372
+ end
373
+
374
+ ###
375
+ # Append +name+ to the "class" attribute value of this Node and
376
+ # return self. The value is simply appended without checking if
377
+ # it is already in the current value. If no "class" attribute
378
+ # exists yet, one is created with the given value.
379
+ #
380
+ # More than one class may be appended at a time, separated by a
381
+ # space.
382
+ def append_class name
383
+ self['class'] = (classes + name.scan(/\S+/)).join(' ')
384
+ self
385
+ end
386
+
387
+ ###
388
+ # Remove +name+ from the "class" attribute value of this Node
389
+ # and return self. If there are many occurrences of the name,
390
+ # they are all removed.
391
+ #
392
+ # More than one class may be removed at a time, separated by a
393
+ # space.
394
+ #
395
+ # If no class name is left after removal, or when +name+ is nil,
396
+ # the "class" attribute is removed from this Node.
397
+ def remove_class name = nil
398
+ if name
399
+ names = classes - name.scan(/\S+/)
400
+ if names.empty?
401
+ delete 'class'
402
+ else
403
+ self['class'] = names.join(' ')
404
+ end
405
+ else
406
+ delete "class"
407
+ end
408
+ self
409
+ end
410
+
353
411
  ###
354
412
  # Remove the attribute named +name+
355
413
  def remove_attribute name
@@ -43,9 +43,14 @@ module Nokogiri
43
43
  end
44
44
 
45
45
  ###
46
- # Returns the index of the first node in self that is == to +node+. Returns nil if no match is found.
47
- def index(node)
48
- each_with_index { |member, j| return j if member == node }
46
+ # Returns the index of the first node in self that is == to +node+ or meets the given block. Returns nil if no match is found.
47
+ def index(node = nil, &block)
48
+ if node
49
+ warn "given block not used" if block_given?
50
+ each_with_index { |member, j| return j if member == node }
51
+ elsif block_given?
52
+ each_with_index { |member, j| return j if yield(member) }
53
+ end
49
54
  nil
50
55
  end
51
56
 
@@ -130,31 +135,37 @@ module Nokogiri
130
135
  end
131
136
 
132
137
  ###
133
- # Append the class attribute +name+ to all Node objects in the NodeSet.
138
+ # Add the class attribute +name+ to all Node objects in the
139
+ # NodeSet.
140
+ #
141
+ # See Nokogiri::XML::Node#add_class for more information.
134
142
  def add_class name
135
143
  each do |el|
136
- classes = el['class'].to_s.split(/\s+/)
137
- el['class'] = classes.push(name).uniq.join " "
144
+ el.add_class(name)
145
+ end
146
+ self
147
+ end
148
+
149
+ ###
150
+ # Append the class attribute +name+ to all Node objects in the
151
+ # NodeSet.
152
+ #
153
+ # See Nokogiri::XML::Node#append_class for more information.
154
+ def append_class name
155
+ each do |el|
156
+ el.append_class(name)
138
157
  end
139
158
  self
140
159
  end
141
160
 
142
161
  ###
143
- # Remove the class attribute +name+ from all Node objects in the NodeSet.
144
- # If +name+ is nil, remove the class attribute from all Nodes in the
162
+ # Remove the class attribute +name+ from all Node objects in the
145
163
  # NodeSet.
164
+ #
165
+ # See Nokogiri::XML::Node#remove_class for more information.
146
166
  def remove_class name = nil
147
167
  each do |el|
148
- if name
149
- classes = el['class'].to_s.split(/\s+/)
150
- if classes.empty?
151
- el.delete 'class'
152
- else
153
- el['class'] = (classes - [name]).uniq.join " "
154
- end
155
- else
156
- el.delete "class"
157
- end
168
+ el.remove_class(name)
158
169
  end
159
170
  self
160
171
  end
@@ -182,10 +193,13 @@ module Nokogiri
182
193
  each { |el| el.delete name }
183
194
  self
184
195
  end
196
+ alias remove_attribute remove_attr
185
197
 
186
198
  ###
187
199
  # Iterate over each node, yielding to +block+
188
200
  def each(&block)
201
+ return to_enum unless block_given?
202
+
189
203
  0.upto(length - 1) do |x|
190
204
  yield self[x]
191
205
  end
@@ -0,0 +1,78 @@
1
+ From c5538465c08a8ea248a370bf55bc39cd3385e4af Mon Sep 17 00:00:00 2001
2
+ From: Mike Dalessio <mike.dalessio@gmail.com>
3
+ Date: Thu, 29 Mar 2018 14:09:00 -0400
4
+ Subject: [PATCH] Revert "Do not URI escape in server side includes"
5
+
6
+ This reverts commit 960f0e275616cadc29671a218d7fb9b69eb35588.
7
+ ---
8
+ HTMLtree.c | 49 +++++++++++--------------------------------------
9
+ 1 file changed, 11 insertions(+), 38 deletions(-)
10
+
11
+ diff --git a/HTMLtree.c b/HTMLtree.c
12
+ index 2fd0c9c..67160c5 100644
13
+ --- a/HTMLtree.c
14
+ +++ b/HTMLtree.c
15
+ @@ -717,49 +717,22 @@ htmlAttrDumpOutput(xmlOutputBufferPtr buf, xmlDocPtr doc, xmlAttrPtr cur,
16
+ (!xmlStrcasecmp(cur->name, BAD_CAST "src")) ||
17
+ ((!xmlStrcasecmp(cur->name, BAD_CAST "name")) &&
18
+ (!xmlStrcasecmp(cur->parent->name, BAD_CAST "a"))))) {
19
+ + xmlChar *escaped;
20
+ xmlChar *tmp = value;
21
+ - /* xmlURIEscapeStr() escapes '"' so it can be safely used. */
22
+ - xmlBufCCat(buf->buffer, "\"");
23
+
24
+ while (IS_BLANK_CH(*tmp)) tmp++;
25
+
26
+ - /* URI Escape everything, except server side includes. */
27
+ - for ( ; ; ) {
28
+ - xmlChar *escaped;
29
+ - xmlChar endChar;
30
+ - xmlChar *end = NULL;
31
+ - xmlChar *start = (xmlChar *)xmlStrstr(tmp, BAD_CAST "<!--");
32
+ - if (start != NULL) {
33
+ - end = (xmlChar *)xmlStrstr(tmp, BAD_CAST "-->");
34
+ - if (end != NULL) {
35
+ - *start = '\0';
36
+ - }
37
+ - }
38
+ -
39
+ - /* Escape the whole string, or until start (set to '\0'). */
40
+ - escaped = xmlURIEscapeStr(tmp, BAD_CAST"@/:=?;#%&,+");
41
+ - if (escaped != NULL) {
42
+ - xmlBufCat(buf->buffer, escaped);
43
+ - xmlFree(escaped);
44
+ - } else {
45
+ - xmlBufCat(buf->buffer, tmp);
46
+ - }
47
+ -
48
+ - if (end == NULL) { /* Everything has been written. */
49
+ - break;
50
+ - }
51
+ -
52
+ - /* Do not escape anything within server side includes. */
53
+ - *start = '<'; /* Restore the first character of "<!--". */
54
+ - end += 3; /* strlen("-->") */
55
+ - endChar = *end;
56
+ - *end = '\0';
57
+ - xmlBufCat(buf->buffer, start);
58
+ - *end = endChar;
59
+ - tmp = end;
60
+ + /*
61
+ + * the < and > have already been escaped at the entity level
62
+ + * And doing so here breaks server side includes
63
+ + */
64
+ + escaped = xmlURIEscapeStr(tmp, BAD_CAST"@/:=?;#%&,+<>");
65
+ + if (escaped != NULL) {
66
+ + xmlBufWriteQuotedString(buf->buffer, escaped);
67
+ + xmlFree(escaped);
68
+ + } else {
69
+ + xmlBufWriteQuotedString(buf->buffer, value);
70
+ }
71
+ -
72
+ - xmlBufCCat(buf->buffer, "\"");
73
+ } else {
74
+ xmlBufWriteQuotedString(buf->buffer, value);
75
+ }
76
+ --
77
+ 2.9.5
78
+
@@ -71,83 +71,83 @@ EOF
71
71
 
72
72
 
73
73
  def test_even
74
- assert_result_rows [2,4,6,8,10,12,14], @parser.search("table/tr:nth(even)")
74
+ assert_result_rows [2,4,6,8,10,12,14], @parser.search("table//tr:nth(even)")
75
75
  end
76
76
 
77
77
  def test_odd
78
- assert_result_rows [1,3,5,7,9,11,13], @parser.search("table/tr:nth(odd)")
78
+ assert_result_rows [1,3,5,7,9,11,13], @parser.search("table//tr:nth(odd)")
79
79
  end
80
80
 
81
81
  def test_n
82
- assert_result_rows((1..14).to_a, @parser.search("table/tr:nth(n)"))
82
+ assert_result_rows((1..14).to_a, @parser.search("table//tr:nth(n)"))
83
83
  end
84
84
 
85
85
  def test_2n
86
- assert_equal @parser.search("table/tr:nth(even)").inner_text, @parser.search("table/tr:nth(2n)").inner_text
86
+ assert_equal @parser.search("table//tr:nth(even)").inner_text, @parser.search("table//tr:nth(2n)").inner_text
87
87
  end
88
88
 
89
89
  def test_2np1
90
- assert_equal @parser.search("table/tr:nth(odd)").inner_text, @parser.search("table/tr:nth(2n+1)").inner_text
90
+ assert_equal @parser.search("table//tr:nth(odd)").inner_text, @parser.search("table//tr:nth(2n+1)").inner_text
91
91
  end
92
92
 
93
93
  def test_4np3
94
- assert_result_rows [3,7,11], @parser.search("table/tr:nth(4n+3)")
94
+ assert_result_rows [3,7,11], @parser.search("table//tr:nth(4n+3)")
95
95
  end
96
96
 
97
97
  def test_3np4
98
- assert_result_rows [4,7,10,13], @parser.search("table/tr:nth(3n+4)")
98
+ assert_result_rows [4,7,10,13], @parser.search("table//tr:nth(3n+4)")
99
99
  end
100
100
 
101
101
  def test_mnp3
102
- assert_result_rows [1,2,3], @parser.search("table/tr:nth(-n+3)")
102
+ assert_result_rows [1,2,3], @parser.search("table//tr:nth(-n+3)")
103
103
  end
104
104
 
105
105
  def test_4nm1
106
- assert_result_rows [3,7,11], @parser.search("table/tr:nth(4n-1)")
106
+ assert_result_rows [3,7,11], @parser.search("table//tr:nth(4n-1)")
107
107
  end
108
108
 
109
109
  def test_np3
110
- assert_result_rows [3,4,5,6,7,8,9,10,11,12,13,14], @parser.search("table/tr:nth(n+3)")
110
+ assert_result_rows [3,4,5,6,7,8,9,10,11,12,13,14], @parser.search("table//tr:nth(n+3)")
111
111
  end
112
112
 
113
113
  def test_first
114
- assert_result_rows [1], @parser.search("table/tr:first")
115
- assert_result_rows [1], @parser.search("table/tr:first()")
114
+ assert_result_rows [1], @parser.search("table//tr:first")
115
+ assert_result_rows [1], @parser.search("table//tr:first()")
116
116
  end
117
117
 
118
118
  def test_last
119
- assert_result_rows [14], @parser.search("table/tr:last")
120
- assert_result_rows [14], @parser.search("table/tr:last()")
119
+ assert_result_rows [14], @parser.search("table//tr:last")
120
+ assert_result_rows [14], @parser.search("table//tr:last()")
121
121
  end
122
122
 
123
123
  def test_first_child
124
124
  assert_result_rows [1], @parser.search("div/b:first-child"), "bold"
125
- assert_result_rows [1], @parser.search("table/tr:first-child")
125
+ assert_result_rows [1], @parser.search("table//tr:first-child")
126
126
  assert_result_rows [2,4], @parser.search("div/h1.c:first-child"), "header"
127
127
  end
128
128
 
129
129
  def test_last_child
130
130
  assert_result_rows [3], @parser.search("div/b:last-child"), "bold"
131
- assert_result_rows [14], @parser.search("table/tr:last-child")
131
+ assert_result_rows [14], @parser.search("table//tr:last-child")
132
132
  assert_result_rows [3,4], @parser.search("div/h1.c:last-child"), "header"
133
133
  end
134
134
 
135
135
  def test_nth_child
136
136
  assert_result_rows [2], @parser.search("div/b:nth-child(3)"), "bold"
137
- assert_result_rows [5], @parser.search("table/tr:nth-child(5)")
137
+ assert_result_rows [5], @parser.search("table//tr:nth-child(5)")
138
138
  assert_result_rows [1,3], @parser.search("div/h1.c:nth-child(2)"), "header"
139
139
  assert_result_rows [3,4], @parser.search("div/i.b:nth-child(2n+1)"), "italic"
140
140
  end
141
141
 
142
142
  def test_first_of_type
143
- assert_result_rows [1], @parser.search("table/tr:first-of-type")
143
+ assert_result_rows [1], @parser.search("table//tr:first-of-type")
144
144
  assert_result_rows [1], @parser.search("div/b:first-of-type"), "bold"
145
145
  assert_result_rows [2], @parser.search("div/b.a:first-of-type"), "bold"
146
146
  assert_result_rows [3], @parser.search("div/i.b:first-of-type"), "italic"
147
147
  end
148
148
 
149
149
  def test_last_of_type
150
- assert_result_rows [14], @parser.search("table/tr:last-of-type")
150
+ assert_result_rows [14], @parser.search("table//tr:last-of-type")
151
151
  assert_result_rows [3], @parser.search("div/b:last-of-type"), "bold"
152
152
  assert_result_rows [2,7], @parser.search("div/i:last-of-type"), "italic"
153
153
  assert_result_rows [2,6,7], @parser.search("div i:last-of-type"), "italic"
@@ -165,8 +165,8 @@ EOF
165
165
  end
166
166
 
167
167
  def test_nth_last_of_type
168
- assert_result_rows [14], @parser.search("table/tr:nth-last-of-type(1)")
169
- assert_result_rows [12], @parser.search("table/tr:nth-last-of-type(3)")
168
+ assert_result_rows [14], @parser.search("table//tr:nth-last-of-type(1)")
169
+ assert_result_rows [12], @parser.search("table//tr:nth-last-of-type(3)")
170
170
  assert_result_rows [2,6,7], @parser.search("div i:nth-last-of-type(1)"), "italic"
171
171
  assert_result_rows [1,5], @parser.search("div i:nth-last-of-type(2)"), "italic"
172
172
  assert_result_rows [4], @parser.search("div/i.b:nth-last-of-type(1)"), "italic"