nokolexbor 0.3.4 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/ext/nokolexbor/extconf.rb +9 -5
  3. data/ext/nokolexbor/nl_attribute.c +46 -0
  4. data/ext/nokolexbor/nl_cdata.c +8 -0
  5. data/ext/nokolexbor/nl_comment.c +6 -0
  6. data/ext/nokolexbor/nl_document.c +53 -7
  7. data/ext/nokolexbor/nl_document_fragment.c +9 -0
  8. data/ext/nokolexbor/nl_error.c +21 -19
  9. data/ext/nokolexbor/nl_node.c +255 -50
  10. data/ext/nokolexbor/nl_node_set.c +56 -1
  11. data/ext/nokolexbor/nl_processing_instruction.c +6 -0
  12. data/ext/nokolexbor/nl_text.c +6 -0
  13. data/ext/nokolexbor/nokolexbor.h +1 -0
  14. data/lib/nokolexbor/document.rb +52 -5
  15. data/lib/nokolexbor/document_fragment.rb +11 -0
  16. data/lib/nokolexbor/node.rb +367 -18
  17. data/lib/nokolexbor/node_set.rb +56 -0
  18. data/lib/nokolexbor/version.rb +1 -1
  19. metadata +2 -24
  20. data/vendor/lexbor/source/lexbor/encoding/base.h +0 -218
  21. data/vendor/lexbor/source/lexbor/encoding/big5.c +0 -42839
  22. data/vendor/lexbor/source/lexbor/encoding/config.cmake +0 -12
  23. data/vendor/lexbor/source/lexbor/encoding/const.h +0 -65
  24. data/vendor/lexbor/source/lexbor/encoding/decode.c +0 -3193
  25. data/vendor/lexbor/source/lexbor/encoding/decode.h +0 -370
  26. data/vendor/lexbor/source/lexbor/encoding/encode.c +0 -1931
  27. data/vendor/lexbor/source/lexbor/encoding/encode.h +0 -377
  28. data/vendor/lexbor/source/lexbor/encoding/encoding.c +0 -252
  29. data/vendor/lexbor/source/lexbor/encoding/encoding.h +0 -475
  30. data/vendor/lexbor/source/lexbor/encoding/euc_kr.c +0 -53883
  31. data/vendor/lexbor/source/lexbor/encoding/gb18030.c +0 -47905
  32. data/vendor/lexbor/source/lexbor/encoding/iso_2022_jp_katakana.c +0 -159
  33. data/vendor/lexbor/source/lexbor/encoding/jis0208.c +0 -22477
  34. data/vendor/lexbor/source/lexbor/encoding/jis0212.c +0 -15787
  35. data/vendor/lexbor/source/lexbor/encoding/multi.h +0 -53
  36. data/vendor/lexbor/source/lexbor/encoding/range.c +0 -71
  37. data/vendor/lexbor/source/lexbor/encoding/range.h +0 -34
  38. data/vendor/lexbor/source/lexbor/encoding/res.c +0 -222
  39. data/vendor/lexbor/source/lexbor/encoding/res.h +0 -34
  40. data/vendor/lexbor/source/lexbor/encoding/single.c +0 -13748
  41. data/vendor/lexbor/source/lexbor/encoding/single.h +0 -116
@@ -17,38 +17,51 @@ module Nokolexbor
17
17
  DOCUMENT_FRAG_NODE = 11
18
18
  NOTATION_NODE = 12
19
19
 
20
+ # @return [Document] The associated {Document} of this node
20
21
  attr_reader :document
21
22
 
22
23
  LOOKS_LIKE_XPATH = %r{^(\./|/|\.\.|\.$)}
23
24
 
25
+ # @return true if this is a {Comment}
24
26
  def comment?
25
27
  type == COMMENT_NODE
26
28
  end
27
29
 
30
+ # @return true if this is a {CDATA}
28
31
  def cdata?
29
32
  type == CDATA_SECTION_NODE
30
33
  end
31
34
 
35
+ # @return true if this is a {ProcessingInstruction}
32
36
  def processing_instruction?
33
37
  type == PI_NODE
34
38
  end
35
39
 
40
+ # @return true if this is a {Text}
36
41
  def text?
37
42
  type == TEXT_NODE
38
43
  end
39
44
 
45
+ # @return true if this is a {DocumentFragment}
40
46
  def fragment?
41
47
  type == DOCUMENT_FRAG_NODE
42
48
  end
43
49
 
50
+ # @return true if this is an {Element}
44
51
  def element?
45
52
  type == ELEMENT_NODE
46
53
  end
47
54
 
55
+ # @return true if this is a {Document}
48
56
  def document?
49
57
  is_a?(Nokolexbor::Document)
50
58
  end
51
59
 
60
+ # Get a list of ancestor Node of this Node
61
+ #
62
+ # @param [String, nil] selector The selector to match ancestors
63
+ #
64
+ # @return [NodeSet] A set of matched ancestor nodes
52
65
  def ancestors(selector = nil)
53
66
  return NodeSet.new(@document) unless respond_to?(:parent)
54
67
  return NodeSet.new(@document) unless parent
@@ -71,10 +84,39 @@ module Nokolexbor
71
84
  end)
72
85
  end
73
86
 
87
+ # Wrap this Node with another node.
88
+ #
89
+ # @param node [String, Node] A string or a node
90
+ # - when {String}:
91
+ # The markup that is parsed and used as the wrapper. If the parsed
92
+ # fragment has multiple roots, the first root node is used as the wrapper.
93
+ # - when {Node}:
94
+ # An element that is cloned and used as the wrapper.
95
+ #
96
+ # @return [Node] +self+, to support chaining of calls.
97
+ #
98
+ # @see NodeSet#wrap
99
+ #
100
+ # @example with a {String} argument:
101
+ #
102
+ # doc = Nokolexbor::HTML('<body><a>123</a></body>')
103
+ # doc.at_css('a').wrap('<div></div>')
104
+ # doc.at_css('body').inner_html
105
+ # # => "<div><a>123</a></div>"
106
+ #
107
+ # @example with a {Node} argument:
108
+ #
109
+ # doc = Nokolexbor::HTML('<body><a>123</a></body>')
110
+ # doc.at_css('a').wrap(doc.create_element('div'))
111
+ # doc.at_css('body').inner_html
112
+ # # => "<div><a>123</a></div>"
113
+ #
74
114
  def wrap(node)
75
115
  case node
76
116
  when String
77
117
  new_parent = fragment(node).child
118
+ when DocumentFragment
119
+ new_parent = node.child
78
120
  when Node
79
121
  new_parent = node.dup
80
122
  else
@@ -91,6 +133,13 @@ module Nokolexbor
91
133
  self
92
134
  end
93
135
 
136
+ # Insert +node_or_tags+ before this Node (as a sibling).
137
+ #
138
+ # @param node_or_tags [Node, DocumentFragment, NodeSet, String] The node to be added.
139
+ #
140
+ # @return [Node,NodeSet] The reparented {Node} (if +node_or_tags+ is a {Node}), or {NodeSet} (if +node_or_tags+ is a {DocumentFragment}, {NodeSet}, or {String}).
141
+ #
142
+ # @see #before
94
143
  def add_previous_sibling(node_or_tags)
95
144
  raise ArgumentError,
96
145
  "A document may not have multiple root nodes." if parent&.document? && !(node_or_tags.comment? || node_or_tags.processing_instruction?)
@@ -98,6 +147,13 @@ module Nokolexbor
98
147
  add_sibling(:previous, node_or_tags)
99
148
  end
100
149
 
150
+ # Insert +node_or_tags+ after this Node (as a sibling).
151
+ #
152
+ # @param node_or_tags [Node, DocumentFragment, NodeSet, String] The node to be added.
153
+ #
154
+ # @return [Node,NodeSet] The reparented {Node} (if +node_or_tags+ is a {Node}), or {NodeSet} (if +node_or_tags+ is a {DocumentFragment}, {NodeSet}, or {String}).
155
+ #
156
+ # @see #after
101
157
  def add_next_sibling(node_or_tags)
102
158
  raise ArgumentError,
103
159
  "A document may not have multiple root nodes." if parent&.document? && !(node_or_tags.comment? || node_or_tags.processing_instruction?)
@@ -105,11 +161,25 @@ module Nokolexbor
105
161
  add_sibling(:next, node_or_tags)
106
162
  end
107
163
 
164
+ # Insert +node_or_tags+ before this Node (as a sibling).
165
+ #
166
+ # @param node_or_tags [Node, DocumentFragment, NodeSet, String] The node to be added.
167
+ #
168
+ # @return [Node] +self+, to support chaining of calls.
169
+ #
170
+ # @see #add_previous_sibling
108
171
  def before(node_or_tags)
109
172
  add_previous_sibling(node_or_tags)
110
173
  self
111
174
  end
112
175
 
176
+ # Insert +node_or_tags+ after this Node (as a sibling).
177
+ #
178
+ # @param node_or_tags [Node, DocumentFragment, NodeSet, String] The node to be added.
179
+ #
180
+ # @return [Node] +self+, to support chaining of calls.
181
+ #
182
+ # @see #add_next_sibling
113
183
  def after(node_or_tags)
114
184
  add_next_sibling(node_or_tags)
115
185
  self
@@ -120,11 +190,25 @@ module Nokolexbor
120
190
  alias_method :next=, :add_next_sibling
121
191
  alias_method :previous=, :add_previous_sibling
122
192
 
193
+ # Add +node_or_tags+ as a child of this Node.
194
+ #
195
+ # @param node_or_tags [Node, DocumentFragment, NodeSet, String] The node to be added.
196
+ #
197
+ # @return [Node] +self+, to support chaining of calls.
198
+ #
199
+ # @see #add_child
123
200
  def <<(node_or_tags)
124
201
  add_child(node_or_tags)
125
202
  self
126
203
  end
127
204
 
205
+ # Add +node+ as the first child of this Node.
206
+ #
207
+ # @param node [Node, DocumentFragment, NodeSet, String] The node to be added.
208
+ #
209
+ # @return [Node,NodeSet] The reparented {Node} (if +node+ is a {Node}), or {NodeSet} (if +node+ is a {DocumentFragment}, {NodeSet}, or {String}).
210
+ #
211
+ # @see #add_child
128
212
  def prepend_child(node)
129
213
  if (first = children.first)
130
214
  # Mimic the error add_child would raise.
@@ -136,83 +220,175 @@ module Nokolexbor
136
220
  end
137
221
  end
138
222
 
223
+ # Traverse self and all children.
224
+ # @yield self and all children to +block+ recursively.
139
225
  def traverse(&block)
140
226
  children.each { |j| j.traverse(&block) }
141
227
  yield(self)
142
228
  end
143
229
 
230
+ # @param selector [String] The selector to match
231
+ #
232
+ # @return true if this Node matches +selector+
144
233
  def matches?(selector)
145
234
  ancestors.last.css(selector).any? { |node| node == self }
146
235
  end
147
236
 
237
+ # Fetch this node's attributes.
238
+ #
239
+ # @return [Hash{String => Attribute}] Hash containing attributes belonging to +self+. The hash keys are String attribute names, and the hash values are {Nokolexbor::Attribute}.
148
240
  def attributes
149
241
  attribute_nodes.each_with_object({}) do |node, hash|
150
242
  hash[node.name] = node
151
243
  end
152
244
  end
153
245
 
246
+ # Replace this Node with +node+.
247
+ #
248
+ # @param node [Node, DocumentFragment, NodeSet, String]
249
+ #
250
+ # @return [Node,NodeSet] The reparented {Node} (if +node+ is a {Node}), or {NodeSet} (if +node+ is a {DocumentFragment}, {NodeSet}, or {String}).
251
+ #
252
+ # @see #swap
154
253
  def replace(node)
155
- if node.is_a?(NodeSet)
156
- node.each { |n| add_sibling(:previous, n) }
157
- else
158
- add_sibling(:previous, node)
159
- end
254
+ ret = add_sibling(:previous, node)
160
255
  remove
256
+ ret
257
+ end
258
+
259
+ # Swap this Node for +node+.
260
+ #
261
+ # @param node [Node, DocumentFragment, NodeSet, String]
262
+ #
263
+ # @return [Node] +self+, to support chaining of calls.
264
+ #
265
+ # @see #replace
266
+ def swap(node)
267
+ replace(node)
268
+ self
161
269
  end
162
270
 
271
+ # Set the content of this Node.
272
+ #
273
+ # @param node [Node, DocumentFragment, NodeSet, String] The node to be added.
274
+ #
275
+ # @see #inner_html=
163
276
  def children=(node)
164
277
  children.remove
165
- if node.is_a?(NodeSet)
166
- node.each { |n| add_child(n) }
167
- else
168
- add_child(node)
169
- end
278
+ add_child(node)
170
279
  end
171
280
 
281
+ # Set the parent Node of this Node.
282
+ #
283
+ # @param parent_node [Node] The parent node.
172
284
  def parent=(parent_node)
173
285
  parent_node.add_child(self)
174
286
  end
175
287
 
288
+ # Iterate over each attribute name and value pair of this Node.
289
+ #
290
+ # @yield [String,String] The name and value of the current attribute.
176
291
  def each
177
292
  attributes.each do |name, node|
178
293
  yield [name, node.value]
179
294
  end
180
295
  end
181
296
 
297
+ # Create a {DocumentFragment} containing +tags+ that is relative to _this_
298
+ # context node.
299
+ #
300
+ # @return [DocumentFragment]
182
301
  def fragment(tags)
183
302
  Nokolexbor::DocumentFragment.new(document, tags, self)
184
303
  end
185
304
 
186
305
  alias_method :inner_html=, :children=
187
306
 
307
+ # Search this object for CSS +rules+. +rules+ must be one or more CSS
308
+ # selectors.
309
+ #
310
+ # This method uses Lexbor as the selector engine. Its performance is much higher than {#xpath} or {#nokogiri_css}.
311
+ #
312
+ # @example
313
+ # node.css('title')
314
+ # node.css('body h1.bold')
315
+ # node.css('div + p.green', 'div#one')
316
+ #
317
+ # @return [NodeSet] The matched set of Nodes.
318
+ #
319
+ # @see #xpath
320
+ # @see #nokogiri_css
188
321
  def css(*args)
189
322
  css_impl(args.join(', '))
190
323
  end
191
324
 
325
+ # Like {#css}, but returns the first match.
326
+ #
327
+ # This method uses Lexbor as the selector engine. Its performance is much higher than {#at_xpath} or {#nokogiri_at_css}.
328
+ #
329
+ # @return [Node, nil] The first matched Node.
330
+ #
331
+ # @see #css
332
+ # @see #nokogiri_at_css
192
333
  def at_css(*args)
193
334
  at_css_impl(args.join(', '))
194
335
  end
195
336
 
337
+ # Search this object for CSS +rules+. +rules+ must be one or more CSS
338
+ # selectors. It supports a mixed syntax of CSS selectors and XPath.
339
+ #
340
+ # This method uses libxml2 as the selector engine. It works the same way as {Nokogiri::Node#css}.
341
+ #
342
+ # @return [NodeSet] The matched set of Nodes.
343
+ #
344
+ # @see #css
196
345
  def nokogiri_css(*args)
197
346
  rules, handler, ns, _ = extract_params(args)
198
347
 
199
348
  nokogiri_css_internal(self, rules, handler, ns)
200
349
  end
201
350
 
351
+ # Like {#nokogiri_css}, but returns the first match.
352
+ #
353
+ # This method uses libxml2 as the selector engine. It works the same way as {Nokogiri::Node#at_css}.
354
+ #
355
+ # @return [Node, nil] The first matched Node.
356
+ #
357
+ # @see #nokogiri_at_css
358
+ # @see #at_css
202
359
  def nokogiri_at_css(*args)
203
360
  nokogiri_css(*args).first
204
361
  end
205
362
 
363
+ # Search this node for XPath +paths+. +paths+ must be one or more XPath
364
+ # queries.
365
+ #
366
+ # It works the same way as {Nokogiri::Node#xpath}.
367
+ #
368
+ # @example
369
+ # node.xpath('.//title')
370
+ #
371
+ # @return [NodeSet] The matched set of Nodes.
206
372
  def xpath(*args)
207
373
  paths, handler, ns, binds = extract_params(args)
208
374
 
209
375
  xpath_internal(self, paths, handler, ns, binds)
210
376
  end
211
377
 
378
+ # Like {#xpath}, but returns the first match.
379
+ #
380
+ # It works the same way as {Nokogiri::Node#at_xpath}.
381
+ #
382
+ # @return [Node, nil] The first matched Node.
383
+ #
384
+ # @see #xpath
212
385
  def at_xpath(*args)
213
386
  xpath(*args).first
214
387
  end
215
388
 
389
+ # Search this object for +paths+. +paths+ must be one or more XPath or CSS selectors.
390
+ #
391
+ # @return [NodeSet] The matched set of Nodes.
216
392
  def search(*args)
217
393
  paths, handler, ns, binds = extract_params(args)
218
394
 
@@ -225,6 +401,11 @@ module Nokolexbor
225
401
 
226
402
  alias_method :/, :search
227
403
 
404
+ # Like {#search}, but returns the first match.
405
+ #
406
+ # @return [Node, nil] The first matched Node.
407
+ #
408
+ # @see #search
228
409
  def at(*args)
229
410
  paths, handler, ns, binds = extract_params(args)
230
411
 
@@ -237,26 +418,148 @@ module Nokolexbor
237
418
 
238
419
  alias_method :%, :at
239
420
 
421
+ # Fetch CSS class names of a Node.
422
+ #
423
+ # This is a convenience function and is equivalent to:
424
+ #
425
+ # node.kwattr_values("class")
426
+ #
427
+ # @see #kwattr_values
428
+ # @see #add_class
429
+ # @see #append_class
430
+ # @see #remove_class
431
+ #
432
+ # @return [Array]
433
+ # The CSS classes present in the Node's "class" attribute. If the
434
+ # attribute is empty or non-existent, the return value is an empty array.
435
+ #
436
+ # @example
437
+ # node.classes # => ["section", "title", "header"]
240
438
  def classes
241
439
  kwattr_values("class")
242
440
  end
243
441
 
442
+ # Ensure CSS classes are present on +self+. Any CSS classes in +names+ that already exist
443
+ # in the "class" attribute are _not_ added. Note that any existing duplicates in the
444
+ # "class" attribute are not removed. Compare with {#append_class}.
445
+ #
446
+ # This is a convenience function and is equivalent to:
447
+ #
448
+ # node.kwattr_add("class", names)
449
+ #
450
+ # @see #kwattr_add
451
+ # @see #classes
452
+ # @see #append_class
453
+ # @see #remove_class
454
+ #
455
+ # @param [String, Array<String>] names
456
+ # CSS class names to be added to the Node's "class" attribute. May be a string containing
457
+ # whitespace-delimited names, or an Array of String names. Any class names already present
458
+ # will not be added. Any class names not present will be added. If no "class" attribute
459
+ # exists, one is created.
460
+ #
461
+ # @return [Node] +self+, to support chaining of calls.
462
+ #
463
+ # @example
464
+ # node.add_class("section") # => <div class="section"></div>
465
+ # node.add_class("section") # => <div class="section"></div> # duplicate not added
466
+ # node.add_class("section header") # => <div class="section header"></div>
467
+ # node.add_class(["section", "header"]) # => <div class="section header"></div>
244
468
  def add_class(names)
245
469
  kwattr_add("class", names)
246
470
  end
247
471
 
472
+ # Add CSS classes to +self+, regardless of duplication. Compare with {#add_class}.
473
+ #
474
+ # This is a convenience function and is equivalent to:
475
+ #
476
+ # node.kwattr_append("class", names)
477
+ #
478
+ # @see #kwattr_append
479
+ # @see #classes
480
+ # @see #add_class
481
+ # @see #remove_class
482
+ #
483
+ # @return [Node] +self+, to support chaining of calls.
248
484
  def append_class(names)
249
485
  kwattr_append("class", names)
250
486
  end
251
487
 
488
+ # Remove CSS classes from this node. Any CSS class names in +css_classes+ that exist in
489
+ # this node's "class" attribute are removed, including any multiple entries.
490
+ #
491
+ # If no CSS classes remain after this operation, or if +css_classes+ is +nil+, the "class"
492
+ # attribute is deleted from the node.
493
+ #
494
+ # This is a convenience function and is equivalent to:
495
+ #
496
+ # node.kwattr_remove("class", css_classes)
497
+ #
498
+ # @see #kwattr_remove
499
+ # @see #classes
500
+ # @see #add_class
501
+ # @see #append_class
502
+ #
503
+ # @param names [String, Array<String>]
504
+ # CSS class names to be removed from the Node's
505
+ # "class" attribute. May be a string containing whitespace-delimited names, or an Array of
506
+ # String names. Any class names already present will be removed. If no CSS classes remain,
507
+ # the "class" attribute is deleted.
508
+ #
509
+ # @return [Node] +self+, to support chaining of calls.
510
+ #
511
+ # @example
512
+ # node.remove_class("section")
513
+ # node.remove_class(["section", "float"])
252
514
  def remove_class(names = nil)
253
515
  kwattr_remove("class", names)
254
516
  end
255
517
 
518
+ # Fetch values from a keyword attribute of a Node.
519
+ #
520
+ # A "keyword attribute" is a node attribute that contains a set of space-delimited
521
+ # values. Perhaps the most familiar example of this is the HTML "class" attribute used to
522
+ # contain CSS classes. But other keyword attributes exist, for instance
523
+ # {the "rel" attribute}[https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel].
524
+ #
525
+ # @see #kwattr_add
526
+ # @#kwattr_append
527
+ # @#kwattr_remove
528
+ #
529
+ # @param attribute_name [String]
530
+ # The name of the keyword attribute to be inspected.
531
+ #
532
+ # @return [Array<String>]
533
+ # The values present in the Node's +attribute_name+ attribute. If the
534
+ # attribute is empty or non-existent, the return value is an empty array.
256
535
  def kwattr_values(attribute_name)
257
536
  keywordify(attr(attribute_name) || [])
258
537
  end
259
538
 
539
+ # Ensure that values are present in a keyword attribute.
540
+ #
541
+ # Any values in +keywords+ that already exist in the Node's attribute values are _not_
542
+ # added. Note that any existing duplicates in the attribute values are not removed. Compare
543
+ # with {#kwattr_append}.
544
+ #
545
+ # A "keyword attribute" is a node attribute that contains a set of space-delimited
546
+ # values. Perhaps the most familiar example of this is the HTML "class" attribute used to
547
+ # contain CSS classes. But other keyword attributes exist, for instance
548
+ # {the "rel" attribute}[https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel].
549
+ #
550
+ # @see #add_class
551
+ # @see #kwattr_values
552
+ # @see #kwattr_append
553
+ # @see #kwattr_remove
554
+ #
555
+ # @param attribute_name [String] The name of the keyword attribute to be modified.
556
+ # @param keywords [String, Array<String>]
557
+ # Keywords to be added to the attribute named +attribute_name+. May be a string containing
558
+ # whitespace-delimited values, or an Array of String values. Any values already present will
559
+ # not be added. Any values not present will be added. If the named attribute does not exist,
560
+ # it is created.
561
+ #
562
+ # @return [Node] +self+, to support chaining of calls.
260
563
  def kwattr_add(attribute_name, keywords)
261
564
  keywords = keywordify(keywords)
262
565
  current_kws = kwattr_values(attribute_name)
@@ -265,6 +568,27 @@ module Nokolexbor
265
568
  self
266
569
  end
267
570
 
571
+ # Add keywords to a Node's keyword attribute, regardless of duplication. Compare with
572
+ # {#kwattr_add}.
573
+ #
574
+ # A "keyword attribute" is a node attribute that contains a set of space-delimited
575
+ # values. Perhaps the most familiar example of this is the HTML "class" attribute used to
576
+ # contain CSS classes. But other keyword attributes exist, for instance
577
+ # {the "rel" attribute}[https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel].
578
+ #
579
+ # @see #add_class
580
+ # @see #kwattr_values
581
+ # @see #kwattr_add
582
+ # @see #kwattr_remove
583
+ #
584
+ # @param attribute_name [String] The name of the keyword attribute to be modified.
585
+ # @param keywords [String, Array<String>]
586
+ # Keywords to be added to the attribute named +attribute_name+. May be a string containing
587
+ # whitespace-delimited values, or an Array of String values. Any values already present will
588
+ # not be added. Any values not present will be added. If the named attribute does not exist,
589
+ # it is created.
590
+ #
591
+ # @return [Node] +self+, to support chaining of calls.
268
592
  def kwattr_append(attribute_name, keywords)
269
593
  keywords = keywordify(keywords)
270
594
  current_kws = kwattr_values(attribute_name)
@@ -273,6 +597,30 @@ module Nokolexbor
273
597
  self
274
598
  end
275
599
 
600
+ # Remove keywords from a keyword attribute. Any matching keywords that exist in the named
601
+ # attribute are removed, including any multiple entries.
602
+ #
603
+ # If no keywords remain after this operation, or if +keywords+ is +nil+, the attribute is
604
+ # deleted from the node.
605
+ #
606
+ # A "keyword attribute" is a node attribute that contains a set of space-delimited
607
+ # values. Perhaps the most familiar example of this is the HTML "class" attribute used to
608
+ # contain CSS classes. But other keyword attributes exist, for instance
609
+ # {the "rel" attribute}[https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel].
610
+ #
611
+ # @see #remove_class
612
+ # @see #kwattr_values
613
+ # @see #kwattr_add
614
+ # @see #kwattr_append
615
+ #
616
+ # @param attribute_name [String] The name of the keyword attribute to be modified.
617
+ # @param keywords [String, Array<String>]
618
+ # Keywords to be added to the attribute named +attribute_name+. May be a string containing
619
+ # whitespace-delimited values, or an Array of String values. Any values already present will
620
+ # not be added. Any values not present will be added. If the named attribute does not exist,
621
+ # it is created.
622
+ #
623
+ # @return [Node] +self+, to support chaining of calls.
276
624
  def kwattr_remove(attribute_name, keywords)
277
625
  if keywords.nil?
278
626
  remove_attr(attribute_name)
@@ -290,6 +638,15 @@ module Nokolexbor
290
638
  self
291
639
  end
292
640
 
641
+ # Serialize Node and write to +io+.
642
+ def write_to(io, *options)
643
+ io.write(to_html(*options))
644
+ end
645
+
646
+ alias_method :write_html_to, :write_to
647
+
648
+ private
649
+
293
650
  def keywordify(keywords)
294
651
  case keywords
295
652
  when Enumerable
@@ -302,14 +659,6 @@ module Nokolexbor
302
659
  end
303
660
  end
304
661
 
305
- def write_to(io, *options)
306
- io.write(to_html(*options))
307
- end
308
-
309
- alias_method :write_html_to, :write_to
310
-
311
- private
312
-
313
662
  def nokogiri_css_internal(node, rules, handler, ns)
314
663
  xpath_internal(node, css_rules_to_xpath(rules, ns), handler, ns, nil)
315
664
  end