makiri 0.2.0 → 0.3.0

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +12 -7
  3. data/CHANGELOG.md +93 -14
  4. data/README.md +173 -7
  5. data/Rakefile +103 -7
  6. data/ext/makiri/bridge/bridge.h +28 -0
  7. data/ext/makiri/bridge/ruby_string.c +217 -0
  8. data/ext/makiri/core/mkr_alloc.h +1 -1
  9. data/ext/makiri/core/mkr_buf.c +35 -1
  10. data/ext/makiri/core/mkr_buf.h +37 -3
  11. data/ext/makiri/core/mkr_core.h +1 -1
  12. data/ext/makiri/core/mkr_hash.h +1 -1
  13. data/ext/makiri/core/mkr_text.h +8 -8
  14. data/ext/makiri/extconf.rb +20 -2
  15. data/ext/makiri/glue/glue.h +47 -11
  16. data/ext/makiri/glue/ruby_doc.c +117 -43
  17. data/ext/makiri/glue/ruby_html_css.c +246 -0
  18. data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +242 -51
  19. data/ext/makiri/glue/ruby_html_node.c +888 -0
  20. data/ext/makiri/glue/ruby_html_serialize.c +154 -0
  21. data/ext/makiri/glue/ruby_node.c +54 -748
  22. data/ext/makiri/glue/ruby_node_set.c +167 -32
  23. data/ext/makiri/glue/ruby_xml.c +420 -0
  24. data/ext/makiri/glue/ruby_xml_node.c +1386 -0
  25. data/ext/makiri/glue/ruby_xpath.c +59 -26
  26. data/ext/makiri/glue/ruby_xpath.h +19 -0
  27. data/ext/makiri/lexbor_compat/compat.h +42 -9
  28. data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
  29. data/ext/makiri/lexbor_compat/dom_index.c +2 -2
  30. data/ext/makiri/lexbor_compat/post_parse.c +100 -10
  31. data/ext/makiri/lexbor_compat/source_loc.c +13 -9
  32. data/ext/makiri/lexbor_compat/text_index.c +14 -8
  33. data/ext/makiri/lexbor_compat/utf8_input.c +85 -26
  34. data/ext/makiri/makiri.c +139 -6
  35. data/ext/makiri/makiri.h +43 -2
  36. data/ext/makiri/xml/mkr_xml.h +126 -0
  37. data/ext/makiri/xml/mkr_xml_chars.c +225 -0
  38. data/ext/makiri/xml/mkr_xml_mutate.c +875 -0
  39. data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
  40. data/ext/makiri/xml/mkr_xml_node.c +267 -0
  41. data/ext/makiri/xml/mkr_xml_node.h +119 -0
  42. data/ext/makiri/xml/mkr_xml_tree.c +1479 -0
  43. data/ext/makiri/xpath/mkr_xpath.c +59 -32
  44. data/ext/makiri/xpath/mkr_xpath.h +96 -4
  45. data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
  46. data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
  47. data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +202 -175
  48. data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +110 -86
  49. data/ext/makiri/xpath/mkr_xpath_internal.h +91 -200
  50. data/ext/makiri/xpath/mkr_xpath_lex.c +2 -2
  51. data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
  52. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +142 -0
  53. data/ext/makiri/xpath/mkr_xpath_parse.c +5 -5
  54. data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
  55. data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
  56. data/ext/makiri/xpath/mkr_xpath_shared.c +593 -0
  57. data/ext/makiri/xpath/{mkr_xpath_value.c → mkr_xpath_value_body.h} +145 -656
  58. data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
  59. data/lib/makiri/{attribute.rb → attr.rb} +7 -3
  60. data/lib/makiri/cdata_section.rb +21 -0
  61. data/lib/makiri/comment.rb +12 -0
  62. data/lib/makiri/compat_aliases.rb +30 -0
  63. data/lib/makiri/document.rb +4 -76
  64. data/lib/makiri/document_fragment.rb +14 -9
  65. data/lib/makiri/element.rb +5 -3
  66. data/lib/makiri/html/document.rb +106 -0
  67. data/lib/makiri/html/node_methods.rb +19 -0
  68. data/lib/makiri/html.rb +12 -0
  69. data/lib/makiri/node.rb +58 -15
  70. data/lib/makiri/node_set.rb +8 -0
  71. data/lib/makiri/processing_instruction.rb +12 -0
  72. data/lib/makiri/text.rb +2 -0
  73. data/lib/makiri/version.rb +1 -1
  74. data/lib/makiri/xml/document.rb +24 -0
  75. data/lib/makiri/xml/node_methods.rb +37 -0
  76. data/lib/makiri/xml.rb +10 -0
  77. data/lib/makiri/xpath_context.rb +1 -1
  78. data/lib/makiri.rb +23 -5
  79. data/script/build_native_gem.rb +2 -2
  80. data/script/check_c_safety.rb +32 -0
  81. data/script/check_c_safety_allowlist.yml +83 -0
  82. metadata +35 -9
  83. data/ext/makiri/glue/ruby_css.c +0 -185
  84. data/ext/makiri/glue/ruby_serialize.c +0 -92
  85. data/lib/makiri/cdata.rb +0 -6
@@ -4,20 +4,41 @@
4
4
  #include <stdint.h>
5
5
 
6
6
  /* MKR_NODE_SET_MAX (the per-set node cap, shared with the CSS/XPath glue) is
7
- * defined in glue.h. Every node-collecting path tree walks
8
- * (children / element_children / attribute_nodes), XPath, and CSS fails
7
+ * defined in glue.h. Every node-collecting path - tree walks
8
+ * (children / element_children / attribute_nodes), XPath, and CSS - fails
9
9
  * closed at that bound instead of growing without limit. */
10
10
 
11
11
  /* A NodeSet is a plain dynamic array of Lexbor node pointers plus a keepalive
12
12
  * reference to the owning Document. Nodes are owned by the document arena, so
13
13
  * marking the document keeps them all alive. */
14
+ /* The stored nodes are mkr_raw_node_t* (representation-opaque; see glue.h): the
15
+ * set never dereferences them, it only compares them for identity and, when
16
+ * vending a node, casts to the representation named by doc_is_xml. This keeps an
17
+ * XML set from ever reading its mkr_xml_node_t* pointers as lxb_dom_node_t. */
14
18
  typedef struct {
15
- lxb_dom_node_t **nodes;
19
+ mkr_raw_node_t **nodes;
16
20
  size_t count;
17
21
  size_t cap;
18
22
  VALUE document;
23
+ int doc_is_xml; /* cached once: the stored pointers are
24
+ * mkr_xml_node_t* (wrap as Makiri::XML::*) */
19
25
  } mkr_node_set_data_t;
20
26
 
27
+ /* Wrap a stored node into a Ruby Node, choosing the representation by the set's
28
+ * (fixed) document kind - an XML document's nodes are custom mkr_xml_node_t. This
29
+ * is the ONLY place a stored raw node is cast back to a typed pointer, and the
30
+ * cast is justified by doc_is_xml. The kind is cached at construction so this
31
+ * stays a single branch per node (a per-node is_kind_of/parsed-kind probe would
32
+ * regress the hot traversal path). */
33
+ static VALUE
34
+ mkr_node_set_wrap(const mkr_node_set_data_t *s, mkr_raw_node_t *node)
35
+ {
36
+ if (s->doc_is_xml) {
37
+ return mkr_wrap_xml_node((struct mkr_xml_node *)node, s->document);
38
+ }
39
+ return mkr_wrap_html_node((lxb_dom_node_t *)node, s->document);
40
+ }
41
+
21
42
  static void
22
43
  mkr_node_set_gc_mark(void *ptr)
23
44
  {
@@ -41,7 +62,7 @@ mkr_node_set_memsize(const void *ptr)
41
62
  const mkr_node_set_data_t *s = (const mkr_node_set_data_t *)ptr;
42
63
  size_t nodes_bytes;
43
64
  size_t total;
44
- if (!mkr_size_mul(s->cap, sizeof(lxb_dom_node_t *), &nodes_bytes)) {
65
+ if (!mkr_size_mul(s->cap, sizeof(mkr_raw_node_t *), &nodes_bytes)) {
45
66
  return sizeof(*s);
46
67
  }
47
68
  if (!mkr_size_add(sizeof(*s), nodes_bytes, &total)) {
@@ -62,15 +83,16 @@ mkr_node_set_new(VALUE document)
62
83
  mkr_node_set_data_t *s;
63
84
  VALUE obj = TypedData_Make_Struct(mkr_cNodeSet, mkr_node_set_data_t,
64
85
  &mkr_node_set_type, s);
65
- s->nodes = NULL;
66
- s->count = 0;
67
- s->cap = 0;
68
- s->document = document;
86
+ s->nodes = NULL;
87
+ s->count = 0;
88
+ s->cap = 0;
89
+ s->document = document;
90
+ s->doc_is_xml = rb_obj_is_kind_of(document, mkr_cXmlDocument) ? 1 : 0;
69
91
  return obj;
70
92
  }
71
93
 
72
94
  void
73
- mkr_node_set_push(VALUE rb_set, lxb_dom_node_t *node)
95
+ mkr_node_set_push(VALUE rb_set, mkr_raw_node_t *node)
74
96
  {
75
97
  mkr_node_set_data_t *s;
76
98
  TypedData_Get_Struct(rb_set, mkr_node_set_data_t, &mkr_node_set_type, s);
@@ -85,10 +107,10 @@ mkr_node_set_push(VALUE rb_set, lxb_dom_node_t *node)
85
107
  * overflow-checked (count * size) allocation while keeping the buffer
86
108
  * GC-accounted and paired with xfree in gc_free. */
87
109
  size_t new_cap;
88
- if (!mkr_grow_capacity(s->cap, s->count + 1, sizeof(lxb_dom_node_t *), &new_cap)) {
110
+ if (!mkr_grow_capacity(s->cap, s->count + 1, sizeof(mkr_raw_node_t *), &new_cap)) {
89
111
  rb_raise(mkr_eError, "node set capacity overflow");
90
112
  }
91
- s->nodes = xrealloc2(s->nodes, new_cap, sizeof(lxb_dom_node_t *));
113
+ s->nodes = xrealloc2(s->nodes, new_cap, sizeof(mkr_raw_node_t *));
92
114
  s->cap = new_cap;
93
115
  }
94
116
  s->nodes[s->count++] = node;
@@ -102,21 +124,52 @@ mkr_node_set_length(VALUE self)
102
124
  return ULONG2NUM(s->count);
103
125
  }
104
126
 
105
- /* set[i] -> Node or nil. Negative indices count from the end. */
127
+ /* A new NodeSet from the [beg, beg+len) run of `s` (caller has clamped them to
128
+ * [0, count]). */
129
+ static VALUE
130
+ mkr_node_set_slice(mkr_node_set_data_t *s, long beg, long len)
131
+ {
132
+ VALUE result = mkr_node_set_new(s->document);
133
+ for (long i = 0; i < len; i++) {
134
+ mkr_node_set_push(result, s->nodes[beg + i]);
135
+ }
136
+ return result;
137
+ }
138
+
139
+ /* set[i] -> Node or nil (negative indices count from the end).
140
+ * set[start, length] -> a new NodeSet (nil if start is out of range).
141
+ * set[range] -> a new NodeSet (nil if the range start is out of range).
142
+ * Mirrors Array#[]. */
106
143
  static VALUE
107
- mkr_node_set_aref(VALUE self, VALUE rb_index)
144
+ mkr_node_set_aref(int argc, VALUE *argv, VALUE self)
108
145
  {
109
146
  mkr_node_set_data_t *s;
110
147
  TypedData_Get_Struct(self, mkr_node_set_data_t, &mkr_node_set_type, s);
111
-
112
- long i = NUM2LONG(rb_index);
113
- if (i < 0) {
114
- i += (long)s->count;
148
+ long count = (long)s->count;
149
+
150
+ if (argc == 2) { /* set[start, length] */
151
+ long beg = NUM2LONG(argv[0]);
152
+ long len = NUM2LONG(argv[1]);
153
+ if (beg < 0) beg += count;
154
+ if (beg < 0 || beg > count || len < 0) return Qnil;
155
+ if (len > count - beg) len = count - beg;
156
+ return mkr_node_set_slice(s, beg, len);
115
157
  }
116
- if (i < 0 || (size_t)i >= s->count) {
117
- return Qnil;
158
+
159
+ rb_check_arity(argc, 1, 2);
160
+
161
+ if (rb_obj_is_kind_of(argv[0], rb_cRange)) {
162
+ long beg, len;
163
+ if (rb_range_beg_len(argv[0], &beg, &len, count, 0) != Qtrue) {
164
+ return Qnil; /* start out of range */
165
+ }
166
+ return mkr_node_set_slice(s, beg, len);
118
167
  }
119
- return mkr_wrap_node(s->nodes[i], s->document);
168
+
169
+ long i = NUM2LONG(argv[0]);
170
+ if (i < 0) i += count;
171
+ if (i < 0 || i >= count) return Qnil;
172
+ return mkr_node_set_wrap(s, s->nodes[i]);
120
173
  }
121
174
 
122
175
  static VALUE
@@ -128,7 +181,7 @@ mkr_node_set_each(VALUE self)
128
181
  RETURN_ENUMERATOR(self, 0, 0);
129
182
 
130
183
  for (size_t i = 0; i < s->count; i++) {
131
- rb_yield(mkr_wrap_node(s->nodes[i], s->document));
184
+ rb_yield(mkr_node_set_wrap(s, s->nodes[i]));
132
185
  }
133
186
  return self;
134
187
  }
@@ -144,8 +197,24 @@ mkr_node_set_get(VALUE v)
144
197
  return s;
145
198
  }
146
199
 
200
+ /* The "other" operand of a set operation, requiring it to share +s+'s document.
201
+ * A result NodeSet borrows exactly one document VALUE (GC keepalive) and one
202
+ * representation flag (HTML vs XML) to wrap its nodes; mixing two documents -
203
+ * which also means possibly mixing HTML and XML - would wrap a node under the
204
+ * wrong representation and fail to keep its document alive. Fail closed instead
205
+ * of silently producing a corrupt set. */
206
+ static mkr_node_set_data_t *
207
+ mkr_node_set_other(const mkr_node_set_data_t *s, VALUE other)
208
+ {
209
+ mkr_node_set_data_t *o = mkr_node_set_get(other);
210
+ if (o->document != s->document) {
211
+ rb_raise(mkr_eError, "cannot combine node sets from different documents");
212
+ }
213
+ return o;
214
+ }
215
+
147
216
  static int
148
- mkr_node_set_member(const mkr_node_set_data_t *s, const lxb_dom_node_t *n)
217
+ mkr_node_set_member(const mkr_node_set_data_t *s, const mkr_raw_node_t *n)
149
218
  {
150
219
  for (size_t i = 0; i < s->count; i++) {
151
220
  if (s->nodes[i] == n) {
@@ -158,10 +227,10 @@ mkr_node_set_member(const mkr_node_set_data_t *s, const lxb_dom_node_t *n)
158
227
  /* Open-addressing pointer-hash set, used to keep the set operators below O(n²)
159
228
  * (a CPU-DoS vector at large operand sizes). NULL is the empty sentinel; DOM
160
229
  * node pointers are never NULL. Sized once for the expected element count (load
161
- * factor < 0.5), so it never rehashes. cap == 0 means "not built" the caller
230
+ * factor < 0.5), so it never rehashes. cap == 0 means "not built" - the caller
162
231
  * then falls back to a linear scan (small operands, or allocation failure). */
163
232
  typedef struct {
164
- const lxb_dom_node_t **slots;
233
+ const mkr_raw_node_t **slots;
165
234
  size_t cap;
166
235
  } mkr_ptrset_t;
167
236
 
@@ -195,7 +264,7 @@ mkr_ptrset_free(mkr_ptrset_t *set)
195
264
 
196
265
  /* Add p; returns 1 if newly added, 0 if already present. */
197
266
  static int
198
- mkr_ptrset_add(mkr_ptrset_t *set, const lxb_dom_node_t *p)
267
+ mkr_ptrset_add(mkr_ptrset_t *set, const mkr_raw_node_t *p)
199
268
  {
200
269
  size_t mask = set->cap - 1;
201
270
  size_t j = mkr_ptr_hash(p) & mask;
@@ -208,7 +277,7 @@ mkr_ptrset_add(mkr_ptrset_t *set, const lxb_dom_node_t *p)
208
277
  }
209
278
 
210
279
  static int
211
- mkr_ptrset_has(const mkr_ptrset_t *set, const lxb_dom_node_t *p)
280
+ mkr_ptrset_has(const mkr_ptrset_t *set, const mkr_raw_node_t *p)
212
281
  {
213
282
  size_t mask = set->cap - 1;
214
283
  size_t j = mkr_ptr_hash(p) & mask;
@@ -230,7 +299,7 @@ static VALUE
230
299
  mkr_node_set_op_or(VALUE self, VALUE other)
231
300
  {
232
301
  mkr_node_set_data_t *s = mkr_node_set_get(self);
233
- mkr_node_set_data_t *o = mkr_node_set_get(other);
302
+ mkr_node_set_data_t *o = mkr_node_set_other(s, other);
234
303
  VALUE result = mkr_node_set_new(s->document);
235
304
  mkr_node_set_data_t *r = mkr_node_set_get(result);
236
305
 
@@ -241,7 +310,7 @@ mkr_node_set_op_or(VALUE self, VALUE other)
241
310
  mkr_node_set_data_t *srcs[2] = { s, o };
242
311
  for (int k = 0; k < 2; k++) {
243
312
  for (size_t i = 0; i < srcs[k]->count; i++) {
244
- lxb_dom_node_t *n = srcs[k]->nodes[i];
313
+ mkr_raw_node_t *n = srcs[k]->nodes[i];
245
314
  int fresh = seen.cap ? mkr_ptrset_add(&seen, n)
246
315
  : !mkr_node_set_member(r, n);
247
316
  if (fresh) mkr_node_set_push(result, n);
@@ -256,7 +325,7 @@ static VALUE
256
325
  mkr_node_set_op_plus(VALUE self, VALUE other)
257
326
  {
258
327
  mkr_node_set_data_t *s = mkr_node_set_get(self);
259
- mkr_node_set_data_t *o = mkr_node_set_get(other);
328
+ mkr_node_set_data_t *o = mkr_node_set_other(s, other);
260
329
  VALUE result = mkr_node_set_new(s->document);
261
330
  for (size_t i = 0; i < s->count; i++) mkr_node_set_push(result, s->nodes[i]);
262
331
  for (size_t i = 0; i < o->count; i++) mkr_node_set_push(result, o->nodes[i]);
@@ -269,7 +338,7 @@ static VALUE
269
338
  mkr_node_set_op_filter(VALUE self, VALUE other, int keep_if_in_other)
270
339
  {
271
340
  mkr_node_set_data_t *s = mkr_node_set_get(self);
272
- mkr_node_set_data_t *o = mkr_node_set_get(other);
341
+ mkr_node_set_data_t *o = mkr_node_set_other(s, other);
273
342
  VALUE result = mkr_node_set_new(s->document);
274
343
  mkr_node_set_data_t *r = mkr_node_set_get(result);
275
344
 
@@ -286,7 +355,7 @@ mkr_node_set_op_filter(VALUE self, VALUE other, int keep_if_in_other)
286
355
  }
287
356
 
288
357
  for (size_t i = 0; i < s->count; i++) {
289
- lxb_dom_node_t *n = s->nodes[i];
358
+ mkr_raw_node_t *n = s->nodes[i];
290
359
  int in_o = oset.cap ? mkr_ptrset_has(&oset, n) : mkr_node_set_member(o, n);
291
360
  if (in_o != keep_if_in_other) continue;
292
361
  int fresh = seen.cap ? mkr_ptrset_add(&seen, n) : !mkr_node_set_member(r, n);
@@ -311,15 +380,81 @@ mkr_node_set_op_minus(VALUE self, VALUE other)
311
380
  return mkr_node_set_op_filter(self, other, 0);
312
381
  }
313
382
 
383
+ /* #dup / #clone: a new NodeSet over the same nodes (the nodes are shared - they
384
+ * are owned by the document arena - but the set itself is independent), like
385
+ * Nokogiri. Defined here because the allocator is undef'd, so Ruby's default
386
+ * allocate-then-copy raises; any level/freeze argument is ignored. */
387
+ static VALUE
388
+ mkr_node_set_dup(int argc, VALUE *argv, VALUE self)
389
+ {
390
+ (void)argc;
391
+ (void)argv;
392
+ mkr_node_set_data_t *s = mkr_node_set_get(self);
393
+ VALUE copy = mkr_node_set_new(s->document);
394
+ /* Reuse the overflow-checked growth + cap enforcement of mkr_node_set_push;
395
+ * the source already has no duplicates, so this is a faithful copy. */
396
+ for (size_t i = 0; i < s->count; i++) {
397
+ mkr_node_set_push(copy, s->nodes[i]);
398
+ }
399
+ return copy;
400
+ }
401
+
402
+ /* NodeSet.new(document_or_node, list = []) -> NodeSet.
403
+ * Mirrors Nokogiri: the first argument is the owning Document (or any node, whose
404
+ * document is taken) that the set pins as a GC keepalive; the optional list seeds
405
+ * it. Every listed node MUST belong to that document - one from another document
406
+ * or representation would be re-wrapped under the wrong document/kind, so it is
407
+ * rejected (fail-closed, preventing the HTML/XML type confusion the set's
408
+ * representation-opaque storage otherwise relies on document kind to avoid). */
409
+ static VALUE
410
+ mkr_node_set_s_new(int argc, VALUE *argv, VALUE klass)
411
+ {
412
+ (void)klass;
413
+ VALUE rb_ctx, rb_list;
414
+ rb_scan_args(argc, argv, "11", &rb_ctx, &rb_list);
415
+
416
+ VALUE rb_doc;
417
+ if (rb_obj_is_kind_of(rb_ctx, mkr_cDocument)) {
418
+ rb_doc = rb_ctx;
419
+ } else if (rb_obj_is_kind_of(rb_ctx, mkr_cNode)) {
420
+ rb_doc = mkr_node_document(rb_ctx);
421
+ } else {
422
+ rb_raise(rb_eTypeError, "expected a Makiri::Document or Node as the first argument");
423
+ }
424
+
425
+ VALUE set = mkr_node_set_new(rb_doc);
426
+ if (NIL_P(rb_list)) {
427
+ return set;
428
+ }
429
+ VALUE arr = rb_check_array_type(rb_list);
430
+ if (NIL_P(arr)) {
431
+ rb_raise(rb_eTypeError, "expected an Array of nodes as the second argument");
432
+ }
433
+ for (long i = 0; i < RARRAY_LEN(arr); i++) {
434
+ VALUE node = RARRAY_AREF(arr, i);
435
+ if (!rb_obj_is_kind_of(node, mkr_cNode) || mkr_node_document(node) != rb_doc) {
436
+ rb_raise(rb_eArgError,
437
+ "every node must be a Makiri node belonging to the given document");
438
+ }
439
+ mkr_node_set_push(set, (mkr_raw_node_t *)mkr_node_raw(node));
440
+ }
441
+ return set;
442
+ }
443
+
314
444
  void
315
445
  mkr_init_node_set(void)
316
446
  {
447
+ rb_undef_alloc_func(mkr_cNodeSet); /* nodes come only from C; .new seeds via the factory */
448
+ rb_define_singleton_method(mkr_cNodeSet, "new", mkr_node_set_s_new, -1);
449
+
317
450
  rb_define_method(mkr_cNodeSet, "|", mkr_node_set_op_or, 1);
318
451
  rb_define_method(mkr_cNodeSet, "+", mkr_node_set_op_plus, 1);
319
452
  rb_define_method(mkr_cNodeSet, "&", mkr_node_set_op_and, 1);
320
453
  rb_define_method(mkr_cNodeSet, "-", mkr_node_set_op_minus, 1);
321
454
 
322
455
  rb_define_method(mkr_cNodeSet, "length", mkr_node_set_length, 0);
323
- rb_define_method(mkr_cNodeSet, "[]", mkr_node_set_aref, 1);
456
+ rb_define_method(mkr_cNodeSet, "[]", mkr_node_set_aref, -1);
324
457
  rb_define_method(mkr_cNodeSet, "each", mkr_node_set_each, 0);
458
+ rb_define_method(mkr_cNodeSet, "dup", mkr_node_set_dup, -1);
459
+ /* #clone is defined in Ruby (node_set.rb) so it can honour `freeze:`. */
325
460
  }