nokolexbor 0.3.3 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/ext/nokolexbor/nl_attribute.c +201 -0
  3. data/ext/nokolexbor/nl_cdata.c +8 -0
  4. data/ext/nokolexbor/nl_comment.c +6 -0
  5. data/ext/nokolexbor/nl_document.c +53 -7
  6. data/ext/nokolexbor/nl_document_fragment.c +9 -0
  7. data/ext/nokolexbor/nl_error.c +21 -19
  8. data/ext/nokolexbor/nl_node.c +317 -48
  9. data/ext/nokolexbor/nl_node_set.c +56 -1
  10. data/ext/nokolexbor/nl_processing_instruction.c +6 -0
  11. data/ext/nokolexbor/nl_text.c +6 -0
  12. data/ext/nokolexbor/nokolexbor.c +1 -0
  13. data/ext/nokolexbor/nokolexbor.h +2 -0
  14. data/lib/nokolexbor/document.rb +52 -5
  15. data/lib/nokolexbor/document_fragment.rb +11 -0
  16. data/lib/nokolexbor/node.rb +370 -24
  17. data/lib/nokolexbor/node_set.rb +56 -0
  18. data/lib/nokolexbor/version.rb +1 -1
  19. data/lib/nokolexbor.rb +0 -1
  20. metadata +3 -25
  21. data/lib/nokolexbor/attribute.rb +0 -18
  22. data/vendor/lexbor/source/lexbor/encoding/base.h +0 -218
  23. data/vendor/lexbor/source/lexbor/encoding/big5.c +0 -42839
  24. data/vendor/lexbor/source/lexbor/encoding/config.cmake +0 -12
  25. data/vendor/lexbor/source/lexbor/encoding/const.h +0 -65
  26. data/vendor/lexbor/source/lexbor/encoding/decode.c +0 -3193
  27. data/vendor/lexbor/source/lexbor/encoding/decode.h +0 -370
  28. data/vendor/lexbor/source/lexbor/encoding/encode.c +0 -1931
  29. data/vendor/lexbor/source/lexbor/encoding/encode.h +0 -377
  30. data/vendor/lexbor/source/lexbor/encoding/encoding.c +0 -252
  31. data/vendor/lexbor/source/lexbor/encoding/encoding.h +0 -475
  32. data/vendor/lexbor/source/lexbor/encoding/euc_kr.c +0 -53883
  33. data/vendor/lexbor/source/lexbor/encoding/gb18030.c +0 -47905
  34. data/vendor/lexbor/source/lexbor/encoding/iso_2022_jp_katakana.c +0 -159
  35. data/vendor/lexbor/source/lexbor/encoding/jis0208.c +0 -22477
  36. data/vendor/lexbor/source/lexbor/encoding/jis0212.c +0 -15787
  37. data/vendor/lexbor/source/lexbor/encoding/multi.h +0 -53
  38. data/vendor/lexbor/source/lexbor/encoding/range.c +0 -71
  39. data/vendor/lexbor/source/lexbor/encoding/range.h +0 -34
  40. data/vendor/lexbor/source/lexbor/encoding/res.c +0 -222
  41. data/vendor/lexbor/source/lexbor/encoding/res.h +0 -34
  42. data/vendor/lexbor/source/lexbor/encoding/single.c +0 -13748
  43. 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,86 +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
 
148
- def attribute(name)
149
- return nil unless key?(name)
150
- Attribute.new(name, attr(name))
151
- end
152
-
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}.
153
240
  def attributes
154
- attrs.map { |k, v| [k, Attribute.new(k, v)] }.to_h
241
+ attribute_nodes.each_with_object({}) do |node, hash|
242
+ hash[node.name] = node
243
+ end
155
244
  end
156
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
157
253
  def replace(node)
158
- if node.is_a?(NodeSet)
159
- node.each { |n| add_sibling(:previous, n) }
160
- else
161
- add_sibling(:previous, node)
162
- end
254
+ ret = add_sibling(:previous, node)
163
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
164
269
  end
165
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=
166
276
  def children=(node)
167
277
  children.remove
168
- if node.is_a?(NodeSet)
169
- node.each { |n| add_child(n) }
170
- else
171
- add_child(node)
172
- end
278
+ add_child(node)
173
279
  end
174
280
 
281
+ # Set the parent Node of this Node.
282
+ #
283
+ # @param parent_node [Node] The parent node.
175
284
  def parent=(parent_node)
176
285
  parent_node.add_child(self)
177
286
  end
178
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.
179
291
  def each
180
292
  attributes.each do |name, node|
181
293
  yield [name, node.value]
182
294
  end
183
295
  end
184
296
 
297
+ # Create a {DocumentFragment} containing +tags+ that is relative to _this_
298
+ # context node.
299
+ #
300
+ # @return [DocumentFragment]
185
301
  def fragment(tags)
186
302
  Nokolexbor::DocumentFragment.new(document, tags, self)
187
303
  end
188
304
 
189
305
  alias_method :inner_html=, :children=
190
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
191
321
  def css(*args)
192
322
  css_impl(args.join(', '))
193
323
  end
194
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
195
333
  def at_css(*args)
196
334
  at_css_impl(args.join(', '))
197
335
  end
198
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
199
345
  def nokogiri_css(*args)
200
346
  rules, handler, ns, _ = extract_params(args)
201
347
 
202
348
  nokogiri_css_internal(self, rules, handler, ns)
203
349
  end
204
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
205
359
  def nokogiri_at_css(*args)
206
360
  nokogiri_css(*args).first
207
361
  end
208
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.
209
372
  def xpath(*args)
210
373
  paths, handler, ns, binds = extract_params(args)
211
374
 
212
375
  xpath_internal(self, paths, handler, ns, binds)
213
376
  end
214
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
215
385
  def at_xpath(*args)
216
386
  xpath(*args).first
217
387
  end
218
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.
219
392
  def search(*args)
220
393
  paths, handler, ns, binds = extract_params(args)
221
394
 
@@ -228,6 +401,11 @@ module Nokolexbor
228
401
 
229
402
  alias_method :/, :search
230
403
 
404
+ # Like {#search}, but returns the first match.
405
+ #
406
+ # @return [Node, nil] The first matched Node.
407
+ #
408
+ # @see #search
231
409
  def at(*args)
232
410
  paths, handler, ns, binds = extract_params(args)
233
411
 
@@ -240,26 +418,148 @@ module Nokolexbor
240
418
 
241
419
  alias_method :%, :at
242
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"]
243
438
  def classes
244
439
  kwattr_values("class")
245
440
  end
246
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>
247
468
  def add_class(names)
248
469
  kwattr_add("class", names)
249
470
  end
250
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.
251
484
  def append_class(names)
252
485
  kwattr_append("class", names)
253
486
  end
254
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"])
255
514
  def remove_class(names = nil)
256
515
  kwattr_remove("class", names)
257
516
  end
258
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.
259
535
  def kwattr_values(attribute_name)
260
536
  keywordify(attr(attribute_name) || [])
261
537
  end
262
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.
263
563
  def kwattr_add(attribute_name, keywords)
264
564
  keywords = keywordify(keywords)
265
565
  current_kws = kwattr_values(attribute_name)
@@ -268,6 +568,27 @@ module Nokolexbor
268
568
  self
269
569
  end
270
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.
271
592
  def kwattr_append(attribute_name, keywords)
272
593
  keywords = keywordify(keywords)
273
594
  current_kws = kwattr_values(attribute_name)
@@ -276,6 +597,30 @@ module Nokolexbor
276
597
  self
277
598
  end
278
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.
279
624
  def kwattr_remove(attribute_name, keywords)
280
625
  if keywords.nil?
281
626
  remove_attr(attribute_name)
@@ -293,6 +638,15 @@ module Nokolexbor
293
638
  self
294
639
  end
295
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
+
296
650
  def keywordify(keywords)
297
651
  case keywords
298
652
  when Enumerable
@@ -305,14 +659,6 @@ module Nokolexbor
305
659
  end
306
660
  end
307
661
 
308
- def write_to(io, *options)
309
- io.write(to_html(*options))
310
- end
311
-
312
- alias_method :write_html_to, :write_to
313
-
314
- private
315
-
316
662
  def nokogiri_css_internal(node, rules, handler, ns)
317
663
  xpath_internal(node, css_rules_to_xpath(rules, ns), handler, ns, nil)
318
664
  end