nokolexbor 0.3.3 → 0.3.5

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 (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