makiri 0.2.0 → 0.4.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +22 -0
  3. data/.github/workflows/libfuzzer.yml +83 -0
  4. data/.github/workflows/release.yml +12 -7
  5. data/.github/workflows/security.yml +88 -3
  6. data/.github/workflows/valgrind.yml +135 -0
  7. data/CHANGELOG.md +152 -15
  8. data/README.md +183 -13
  9. data/Rakefile +294 -7
  10. data/ext/makiri/bridge/bridge.h +28 -0
  11. data/ext/makiri/bridge/ruby_string.c +282 -12
  12. data/ext/makiri/core/mkr_alloc.c +40 -3
  13. data/ext/makiri/core/mkr_alloc.h +28 -5
  14. data/ext/makiri/core/mkr_buf.c +47 -3
  15. data/ext/makiri/core/mkr_buf.h +112 -3
  16. data/ext/makiri/core/mkr_core.c +143 -0
  17. data/ext/makiri/core/mkr_core.h +11 -2
  18. data/ext/makiri/core/mkr_hash.h +1 -1
  19. data/ext/makiri/core/mkr_span.h +186 -0
  20. data/ext/makiri/core/mkr_text.h +8 -8
  21. data/ext/makiri/core/mkr_utf8.c +101 -0
  22. data/ext/makiri/core/mkr_utf8.h +88 -0
  23. data/ext/makiri/extconf.rb +123 -10
  24. data/ext/makiri/fuzz/Makefile +95 -0
  25. data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
  26. data/ext/makiri/fuzz/xml_fuzz.c +24 -0
  27. data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
  28. data/ext/makiri/glue/glue.h +55 -11
  29. data/ext/makiri/glue/ruby_doc.c +129 -59
  30. data/ext/makiri/glue/ruby_html_css.c +292 -0
  31. data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
  32. data/ext/makiri/glue/ruby_html_node.c +859 -0
  33. data/ext/makiri/glue/ruby_html_serialize.c +154 -0
  34. data/ext/makiri/glue/ruby_node.c +74 -729
  35. data/ext/makiri/glue/ruby_node_set.c +167 -32
  36. data/ext/makiri/glue/ruby_xml.c +602 -0
  37. data/ext/makiri/glue/ruby_xml_node.c +1373 -0
  38. data/ext/makiri/glue/ruby_xpath.c +63 -30
  39. data/ext/makiri/glue/ruby_xpath.h +19 -0
  40. data/ext/makiri/lexbor_compat/compat.h +42 -9
  41. data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
  42. data/ext/makiri/lexbor_compat/dom_index.c +2 -2
  43. data/ext/makiri/lexbor_compat/post_parse.c +100 -10
  44. data/ext/makiri/lexbor_compat/source_loc.c +15 -13
  45. data/ext/makiri/lexbor_compat/text_index.c +14 -8
  46. data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
  47. data/ext/makiri/makiri.c +184 -6
  48. data/ext/makiri/makiri.h +43 -2
  49. data/ext/makiri/xml/mkr_xml.h +125 -0
  50. data/ext/makiri/xml/mkr_xml_chars.c +195 -0
  51. data/ext/makiri/xml/mkr_xml_index.c +169 -0
  52. data/ext/makiri/xml/mkr_xml_index.h +48 -0
  53. data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
  54. data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
  55. data/ext/makiri/xml/mkr_xml_node.c +399 -0
  56. data/ext/makiri/xml/mkr_xml_node.h +184 -0
  57. data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
  58. data/ext/makiri/xpath/mkr_css.c +1023 -0
  59. data/ext/makiri/xpath/mkr_css.h +65 -0
  60. data/ext/makiri/xpath/mkr_xpath.c +96 -32
  61. data/ext/makiri/xpath/mkr_xpath.h +109 -4
  62. data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
  63. data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
  64. data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
  65. data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
  66. data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
  67. data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
  68. data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
  69. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
  70. data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
  71. data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
  72. data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
  73. data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
  74. data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
  75. data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
  76. data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
  77. data/lib/makiri/{attribute.rb → attr.rb} +7 -3
  78. data/lib/makiri/cdata_section.rb +19 -0
  79. data/lib/makiri/comment.rb +10 -0
  80. data/lib/makiri/compat_aliases.rb +30 -0
  81. data/lib/makiri/document.rb +9 -73
  82. data/lib/makiri/document_fragment.rb +14 -9
  83. data/lib/makiri/element.rb +4 -4
  84. data/lib/makiri/html/document.rb +106 -0
  85. data/lib/makiri/html/node_methods.rb +19 -0
  86. data/lib/makiri/html.rb +12 -0
  87. data/lib/makiri/node.rb +58 -15
  88. data/lib/makiri/node_set.rb +8 -0
  89. data/lib/makiri/processing_instruction.rb +10 -0
  90. data/lib/makiri/text.rb +1 -1
  91. data/lib/makiri/version.rb +1 -1
  92. data/lib/makiri/xml/builder.rb +263 -0
  93. data/lib/makiri/xml/document.rb +24 -0
  94. data/lib/makiri/xml/node_methods.rb +84 -0
  95. data/lib/makiri/xml.rb +10 -0
  96. data/lib/makiri/xpath_context.rb +1 -1
  97. data/lib/makiri.rb +24 -5
  98. data/script/build_native_gem.rb +2 -2
  99. data/script/check_alloc_failures.rb +266 -0
  100. data/script/check_c_safety.rb +77 -2
  101. data/script/check_c_safety_allowlist.yml +102 -0
  102. data/script/check_leaks.rb +64 -0
  103. data/script/leaks_harness.rb +64 -0
  104. data/vendor/lexbor/CMakeLists.txt +6 -0
  105. data/vendor/lexbor/README.md +12 -0
  106. data/vendor/lexbor/config.cmake +1 -1
  107. data/vendor/lexbor/source/lexbor/core/base.h +1 -1
  108. data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
  109. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
  110. data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
  111. data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
  112. data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
  113. data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
  114. data/vendor/lexbor/source/lexbor/html/base.h +1 -1
  115. data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
  116. data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
  117. data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
  118. data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
  119. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
  120. data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
  121. data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
  122. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  123. data/vendor/lexbor/source/lexbor/url/url.c +5 -2
  124. data/vendor/lexbor/source/lexbor/url/url.h +9 -0
  125. data/vendor/lexbor/version +1 -1
  126. metadata +53 -9
  127. data/ext/makiri/glue/ruby_css.c +0 -185
  128. data/ext/makiri/glue/ruby_serialize.c +0 -92
  129. data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
  130. data/lib/makiri/cdata.rb +0 -6
@@ -0,0 +1,1373 @@
1
+ /* ruby_xml_node.c - Ruby read + mutation API for custom XML nodes.
2
+ *
3
+ * The XML counterpart of ruby_node.c: it wraps a mkr_xml_node_t into the right
4
+ * Makiri::XML::* leaf and defines the reader/query/mutation methods on the
5
+ * Makiri::XML::NodeMethods behavior module (included into every XML leaf), each
6
+ * reading the custom node's fields directly. XML nodes never inherit the lxb_dom
7
+ * HTML readers (those live on Makiri::HTML::NodeMethods), so the surface is
8
+ * structural; the in-place edits (remove/[]=/delete/content=/name=) route to the
9
+ * Ruby-free primitives in xml/mkr_xml_mutate.c. The shared node TypedData (mkr_node_type) stores the
10
+ * node pointer + a keepalive Document VALUE; for XML the pointer is a
11
+ * mkr_xml_node_t* (the document arena outlives the wrapper via the Document).
12
+ */
13
+ #include "glue.h"
14
+ #include "../xml/mkr_xml_node.h"
15
+ #include "../xml/mkr_xml_mutate.h"
16
+ #include "../xml/mkr_xml_index.h" /* element-name index invalidation on mutation */
17
+ #include "../core/mkr_core.h" /* mkr_buf */
18
+
19
+ #include <ruby/encoding.h> /* rb_to_encoding / rb_str_encode (output encoding) */
20
+ #include <stdlib.h> /* qsort / free (C14N attribute sort) */
21
+ #include <string.h>
22
+
23
+ /* ---- wrap / unwrap ---- */
24
+
25
+ VALUE
26
+ mkr_wrap_xml_node(mkr_xml_node_t *node, VALUE document)
27
+ {
28
+ if (node == NULL) {
29
+ return Qnil;
30
+ }
31
+ if (node->type == MKR_XML_NODE_TYPE_DOCUMENT) {
32
+ return document; /* the document node maps back onto the Ruby Document */
33
+ }
34
+ VALUE klass;
35
+ switch (node->type) {
36
+ case MKR_XML_NODE_TYPE_ELEMENT: klass = mkr_cXmlElement; break;
37
+ case MKR_XML_NODE_TYPE_ATTRIBUTE: klass = mkr_cXmlAttr; break;
38
+ case MKR_XML_NODE_TYPE_TEXT: klass = mkr_cXmlText; break;
39
+ case MKR_XML_NODE_TYPE_CDATA_SECTION: klass = mkr_cXmlCDATASection; break;
40
+ case MKR_XML_NODE_TYPE_COMMENT: klass = mkr_cXmlComment; break;
41
+ case MKR_XML_NODE_TYPE_PI: klass = mkr_cXmlProcessingInstruction; break;
42
+ case MKR_XML_NODE_TYPE_DOCUMENT_TYPE: klass = mkr_cXmlDocumentType; break;
43
+ case MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT: klass = mkr_cXmlDocumentFragment; break;
44
+ default: klass = mkr_cXmlNode; break;
45
+ }
46
+ mkr_node_data_t *nd;
47
+ VALUE obj = TypedData_Make_Struct(klass, mkr_node_data_t, &mkr_xml_node_type, nd);
48
+ nd->node = (mkr_raw_node_t *)node; /* an mkr_xml_node_t*; XML readers cast back */
49
+ nd->document = document;
50
+ return obj;
51
+ }
52
+
53
+ /* The XML node-pointer accessor (the counterpart of mkr_html_node_unwrap): returns the
54
+ * mkr_xml_node_t for an XML node or XML Document, and RAISES TypeError for an HTML
55
+ * node/Document (TypedData_Get_Struct checks mkr_xml_node_type, which an HTML node
56
+ * - wrapped under mkr_html_node_type - does not satisfy). Non-static so the shared
57
+ * XPath glue can resolve an XML context/result node safely. */
58
+ mkr_xml_node_t *
59
+ mkr_xml_node_unwrap(VALUE self)
60
+ {
61
+ if (rb_obj_is_kind_of(self, mkr_cXmlDocument)) {
62
+ return mkr_parsed_xml_doc(mkr_doc_parsed(self))->doc_node;
63
+ }
64
+ mkr_node_data_t *nd;
65
+ TypedData_Get_Struct(self, mkr_node_data_t, &mkr_xml_node_type, nd);
66
+ return (mkr_xml_node_t *)nd->node;
67
+ }
68
+
69
+ static VALUE
70
+ mkr_xml_node_document(VALUE self)
71
+ {
72
+ if (rb_obj_is_kind_of(self, mkr_cXmlDocument)) {
73
+ return self;
74
+ }
75
+ mkr_node_data_t *nd; /* XML-strict: rejects a non-XML node at the type boundary */
76
+ TypedData_Get_Struct(self, mkr_node_data_t, &mkr_xml_node_type, nd);
77
+ return nd->document;
78
+ }
79
+
80
+ /* ---- name / namespace ---- */
81
+
82
+ static VALUE
83
+ mkr_xml_node_name(VALUE self)
84
+ {
85
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
86
+ switch (n->type) {
87
+ case MKR_XML_NODE_TYPE_ELEMENT:
88
+ case MKR_XML_NODE_TYPE_ATTRIBUTE: return rb_utf8_str_new(n->qname, (long)n->qname_len);
89
+ case MKR_XML_NODE_TYPE_PI: return rb_utf8_str_new(n->local, (long)n->local_len); /* target */
90
+ case MKR_XML_NODE_TYPE_TEXT: return rb_utf8_str_new_cstr("text");
91
+ case MKR_XML_NODE_TYPE_CDATA_SECTION: return rb_utf8_str_new_cstr("#cdata-section");
92
+ case MKR_XML_NODE_TYPE_COMMENT: return rb_utf8_str_new_cstr("comment");
93
+ case MKR_XML_NODE_TYPE_DOCUMENT_TYPE: return rb_utf8_str_new(n->local, (long)n->local_len); /* the DOCTYPE name */
94
+ case MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT: return rb_utf8_str_new_cstr("#document-fragment");
95
+ default: return rb_utf8_str_new_cstr("document");
96
+ }
97
+ }
98
+
99
+ /* ---- DTD (DOCUMENT_TYPE) identifiers ----
100
+ *
101
+ * The off-tree doctype node repurposes fields: local/qname = the DOCTYPE name
102
+ * (Node#name), prefix = the PUBLIC/external id, value = the SYSTEM id. A field
103
+ * left NULL means that id was absent (-> nil); an empty literal (e.g. PUBLIC "")
104
+ * is a non-NULL 0-length slice (-> ""). Mirrors Nokogiri::XML::DTD#external_id /
105
+ * #system_id; #public_id is a WHATWG-DOM-style alias of #external_id. The DTD
106
+ * itself is NOT parsed (no entities/elements), so there is nothing else to read. */
107
+ static VALUE
108
+ mkr_xml_dtd_external_id(VALUE self)
109
+ {
110
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
111
+ return n->prefix == NULL ? Qnil : rb_utf8_str_new(n->prefix, (long)n->prefix_len);
112
+ }
113
+
114
+ static VALUE
115
+ mkr_xml_dtd_system_id(VALUE self)
116
+ {
117
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
118
+ return n->value == NULL ? Qnil : rb_utf8_str_new(n->value, (long)n->value_len);
119
+ }
120
+
121
+ static VALUE
122
+ mkr_xml_node_local_name(VALUE self)
123
+ {
124
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
125
+ if (n->type == MKR_XML_NODE_TYPE_ELEMENT || n->type == MKR_XML_NODE_TYPE_ATTRIBUTE) {
126
+ return rb_utf8_str_new(n->local, (long)n->local_len);
127
+ }
128
+ return Qnil;
129
+ }
130
+
131
+ static VALUE
132
+ mkr_xml_node_prefix(VALUE self)
133
+ {
134
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
135
+ return n->prefix_len ? rb_utf8_str_new(n->prefix, (long)n->prefix_len) : Qnil;
136
+ }
137
+
138
+ static VALUE
139
+ mkr_xml_node_namespace_uri(VALUE self)
140
+ {
141
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
142
+ return n->ns_uri_len ? rb_utf8_str_new(n->ns_uri, (long)n->ns_uri_len) : Qnil;
143
+ }
144
+
145
+ /* ---- namespace introspection (Nokogiri-compatible) ----
146
+ *
147
+ * Makiri::XML::Namespace is a small (prefix, href) value object. xmlns
148
+ * declarations are stored as ordinary attribute nodes (qname "xmlns" /
149
+ * "xmlns:PREFIX"), so the four queries below just read the tree:
150
+ * #namespace -> the node's own resolved namespace, or nil
151
+ * #namespace_definitions -> the xmlns declarations ON this element
152
+ * #namespaces -> all xmlns declarations IN SCOPE here (a Hash)
153
+ * #collect_namespaces -> every xmlns declaration in the document (a Hash) */
154
+ static VALUE mkr_cXmlNamespace;
155
+
156
+ static VALUE
157
+ mkr_ns_new(VALUE prefix, VALUE href)
158
+ {
159
+ VALUE ns = rb_obj_alloc(mkr_cXmlNamespace);
160
+ rb_ivar_set(ns, rb_intern("@prefix"), prefix);
161
+ rb_ivar_set(ns, rb_intern("@href"), href);
162
+ return ns;
163
+ }
164
+
165
+ static VALUE mkr_ns_prefix(VALUE self) { return rb_ivar_get(self, rb_intern("@prefix")); }
166
+ static VALUE mkr_ns_href(VALUE self) { return rb_ivar_get(self, rb_intern("@href")); }
167
+
168
+ static VALUE
169
+ mkr_ns_equal(VALUE self, VALUE other)
170
+ {
171
+ if (!rb_obj_is_kind_of(other, mkr_cXmlNamespace)) return Qfalse;
172
+ return (rb_equal(mkr_ns_prefix(self), mkr_ns_prefix(other)) &&
173
+ rb_equal(mkr_ns_href(self), mkr_ns_href(other))) ? Qtrue : Qfalse;
174
+ }
175
+
176
+ static VALUE
177
+ mkr_ns_hash(VALUE self)
178
+ {
179
+ return rb_funcall(rb_ary_new3(2, mkr_ns_prefix(self), mkr_ns_href(self)), rb_intern("hash"), 0);
180
+ }
181
+
182
+ static VALUE
183
+ mkr_ns_inspect(VALUE self)
184
+ {
185
+ return rb_sprintf("#<Makiri::XML::Namespace prefix=%" PRIsVALUE " href=%" PRIsVALUE ">",
186
+ rb_inspect(mkr_ns_prefix(self)), rb_inspect(mkr_ns_href(self)));
187
+ }
188
+
189
+ /* The xmlns-declaration detector lives in the node layer (mkr_xml_node_xmlns_decl)
190
+ * so the namespace introspection, the C14N walk, and the mutation namespace
191
+ * resolver all share one definition. */
192
+
193
+ static VALUE
194
+ mkr_xml_node_namespace(VALUE self)
195
+ {
196
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
197
+ if ((n->type == MKR_XML_NODE_TYPE_ELEMENT || n->type == MKR_XML_NODE_TYPE_ATTRIBUTE)
198
+ && n->ns_uri_len > 0) {
199
+ VALUE prefix = n->prefix_len ? rb_utf8_str_new(n->prefix, (long)n->prefix_len) : Qnil;
200
+ return mkr_ns_new(prefix, rb_utf8_str_new(n->ns_uri, (long)n->ns_uri_len));
201
+ }
202
+ return Qnil;
203
+ }
204
+
205
+ static VALUE
206
+ mkr_xml_node_namespace_definitions(VALUE self)
207
+ {
208
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
209
+ VALUE arr = rb_ary_new();
210
+ if (n->type == MKR_XML_NODE_TYPE_ELEMENT) {
211
+ for (mkr_xml_node_t *a = n->attrs; a != NULL; a = a->next) {
212
+ const char *p, *u; uint32_t pl, ul;
213
+ if (mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) {
214
+ VALUE prefix = pl ? rb_utf8_str_new(p, (long)pl) : Qnil;
215
+ rb_ary_push(arr, mkr_ns_new(prefix, rb_utf8_str_new(u, (long)ul)));
216
+ }
217
+ }
218
+ }
219
+ return arr;
220
+ }
221
+
222
+ static VALUE
223
+ mkr_xml_node_namespaces(VALUE self)
224
+ {
225
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
226
+ VALUE h = rb_hash_new();
227
+ for (mkr_xml_node_t *e = n; e != NULL; e = e->parent) { /* inner scope wins */
228
+ if (e->type != MKR_XML_NODE_TYPE_ELEMENT) continue;
229
+ for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
230
+ const char *p, *u; uint32_t pl, ul;
231
+ if (!mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) continue;
232
+ VALUE key = rb_utf8_str_new(a->qname, (long)a->qname_len); /* "xmlns" / "xmlns:p" */
233
+ if (rb_hash_lookup2(h, key, Qundef) == Qundef) {
234
+ rb_hash_aset(h, key, rb_utf8_str_new(u, (long)ul));
235
+ }
236
+ }
237
+ }
238
+ return h;
239
+ }
240
+
241
+ static VALUE
242
+ mkr_xml_node_collect_namespaces(VALUE self)
243
+ {
244
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
245
+ mkr_xml_node_t *root = n;
246
+ while (root->parent != NULL) root = root->parent; /* the DOCUMENT node */
247
+ VALUE h = rb_hash_new();
248
+ /* iterative pre-order over the whole tree (parent-pointer walk, no recursion) */
249
+ for (mkr_xml_node_t *cur = root; cur != NULL;) {
250
+ if (cur->type == MKR_XML_NODE_TYPE_ELEMENT) {
251
+ for (mkr_xml_node_t *a = cur->attrs; a != NULL; a = a->next) {
252
+ const char *p, *u; uint32_t pl, ul;
253
+ if (!mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) continue;
254
+ rb_hash_aset(h, rb_utf8_str_new(a->qname, (long)a->qname_len),
255
+ rb_utf8_str_new(u, (long)ul));
256
+ }
257
+ }
258
+ if (cur->first_child != NULL) { cur = cur->first_child; continue; }
259
+ while (cur != root && cur->next == NULL) cur = cur->parent;
260
+ if (cur == root) break;
261
+ cur = cur->next;
262
+ }
263
+ return h;
264
+ }
265
+
266
+ static VALUE
267
+ mkr_xml_node_node_type(VALUE self)
268
+ {
269
+ return INT2NUM((int)mkr_xml_node_unwrap(self)->type);
270
+ }
271
+
272
+ /* ---- content / text ---- */
273
+
274
+ static VALUE
275
+ mkr_xml_node_content(VALUE self)
276
+ {
277
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
278
+ /* leaf data nodes return their own value verbatim. */
279
+ if (n->type == MKR_XML_NODE_TYPE_TEXT || n->type == MKR_XML_NODE_TYPE_CDATA_SECTION
280
+ || n->type == MKR_XML_NODE_TYPE_COMMENT || n->type == MKR_XML_NODE_TYPE_ATTRIBUTE
281
+ || n->type == MKR_XML_NODE_TYPE_PI) {
282
+ return rb_utf8_str_new(n->value ? n->value : "", (long)n->value_len);
283
+ }
284
+ /* element / document: concatenate every TEXT/CDATA descendant in document
285
+ * order. Iterative pre-order (parent-pointer) walk - no C recursion, so a
286
+ * deep tree cannot overflow the stack. */
287
+ VALUE str = rb_utf8_str_new("", 0);
288
+ mkr_xml_node_t *cur = n->first_child;
289
+ while (cur != NULL) {
290
+ if ((cur->type == MKR_XML_NODE_TYPE_TEXT || cur->type == MKR_XML_NODE_TYPE_CDATA_SECTION) && cur->value_len) {
291
+ rb_str_cat(str, cur->value, (long)cur->value_len);
292
+ }
293
+ if (cur->first_child != NULL) { cur = cur->first_child; continue; }
294
+ while (cur != NULL && cur != n && cur->next == NULL) cur = cur->parent;
295
+ if (cur == NULL || cur == n) break;
296
+ cur = cur->next;
297
+ }
298
+ return str;
299
+ }
300
+
301
+ static VALUE
302
+ mkr_xml_node_value(VALUE self)
303
+ {
304
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
305
+ return rb_utf8_str_new(n->value ? n->value : "", (long)n->value_len);
306
+ }
307
+
308
+ /* ---- navigation ---- */
309
+
310
+ static VALUE
311
+ mkr_xml_wrap_rel(VALUE self, mkr_xml_node_t *rel)
312
+ {
313
+ return mkr_wrap_xml_node(rel, mkr_xml_node_document(self));
314
+ }
315
+
316
+ static VALUE mkr_xml_node_parent(VALUE self) { return mkr_xml_wrap_rel(self, mkr_xml_node_unwrap(self)->parent); }
317
+ static VALUE mkr_xml_node_next(VALUE self) { return mkr_xml_wrap_rel(self, mkr_xml_node_unwrap(self)->next); }
318
+ static VALUE mkr_xml_node_previous(VALUE self) { return mkr_xml_wrap_rel(self, mkr_xml_node_unwrap(self)->prev); }
319
+ static VALUE mkr_xml_node_first_child(VALUE self) { return mkr_xml_wrap_rel(self, mkr_xml_node_unwrap(self)->first_child); }
320
+ static VALUE mkr_xml_node_last_child(VALUE self) { return mkr_xml_wrap_rel(self, mkr_xml_node_unwrap(self)->last_child); }
321
+
322
+ static VALUE
323
+ mkr_xml_node_children(VALUE self)
324
+ {
325
+ VALUE doc = mkr_xml_node_document(self);
326
+ VALUE set = mkr_node_set_new(doc);
327
+ for (mkr_xml_node_t *c = mkr_xml_node_unwrap(self)->first_child; c != NULL; c = c->next) {
328
+ mkr_node_set_push(set, (mkr_raw_node_t *)c);
329
+ }
330
+ return set;
331
+ }
332
+
333
+ /* ---- attributes ---- */
334
+
335
+ static VALUE
336
+ mkr_xml_node_aref(VALUE self, VALUE rb_name)
337
+ {
338
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
339
+ if (n->type != MKR_XML_NODE_TYPE_ELEMENT) return Qnil;
340
+ mkr_ruby_borrowed_text_t nv = mkr_ruby_verified_text(rb_name, "attribute name");
341
+ VALUE out = Qnil;
342
+ for (mkr_xml_node_t *a = n->attrs; a != NULL; a = a->next) {
343
+ if (mkr_bytes_eq(a->qname, a->qname_len, nv.ptr, nv.len)) {
344
+ out = rb_utf8_str_new(a->value ? a->value : "", (long)a->value_len);
345
+ break;
346
+ }
347
+ }
348
+ RB_GC_GUARD(nv.value);
349
+ return out;
350
+ }
351
+
352
+ static VALUE
353
+ mkr_xml_node_attribute_nodes(VALUE self)
354
+ {
355
+ VALUE doc = mkr_xml_node_document(self);
356
+ VALUE set = mkr_node_set_new(doc);
357
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
358
+ if (n->type == MKR_XML_NODE_TYPE_ELEMENT) {
359
+ for (mkr_xml_node_t *a = n->attrs; a != NULL; a = a->next) {
360
+ mkr_node_set_push(set, (mkr_raw_node_t *)a);
361
+ }
362
+ }
363
+ return set;
364
+ }
365
+
366
+ static VALUE
367
+ mkr_xml_node_get_document(VALUE self)
368
+ {
369
+ return mkr_xml_node_document(self);
370
+ }
371
+
372
+ /* ---- XML serialization (#to_xml / #to_s) ---------------------------------
373
+ *
374
+ * Re-emit the parsed tree as XML 1.0 text (UTF-8). The default preserves the
375
+ * content as parsed (no pretty-printing); `pretty: true` indents element-only
376
+ * content (an element with any text/CDATA child stays inline, so character data
377
+ * is never altered). Output is always well-formed and re-parses to the same
378
+ * tree. xmlns declarations ride along as ordinary attribute nodes, so namespaces
379
+ * round-trip without special handling. The tree depth is bounded at parse time
380
+ * (MKR_XML_MAX_DEPTH) and the tree is read-only, so the recursive walk cannot
381
+ * exceed that depth. */
382
+
383
+ #define MKR_XSER_APPEND(b, p, n) \
384
+ do { if (mkr_buf_append((b), (p), (n)) != MKR_OK) return -1; } while (0)
385
+ #define MKR_XSER_LIT(b, lit) MKR_XSER_APPEND((b), (lit), sizeof(lit) - 1)
386
+
387
+ /* Append [s, s+n), escaping &, <, > (and, in an attribute value, " and the
388
+ * whitespace TAB/LF that attribute-value normalization would otherwise fold).
389
+ * CR is always escaped so line-ending normalization cannot alter it on reparse. */
390
+ static int
391
+ mkr_xser_escaped(mkr_buf_t *b, const char *s, uint32_t n, int attr)
392
+ {
393
+ uint32_t start = 0;
394
+ for (uint32_t i = 0; i < n; i++) {
395
+ const char *rep = NULL;
396
+ size_t replen = 0;
397
+ switch (s[i]) {
398
+ case '&': rep = "&amp;"; replen = 5; break;
399
+ case '<': rep = "&lt;"; replen = 4; break;
400
+ case '>': rep = "&gt;"; replen = 4; break;
401
+ case '"': if (attr) { rep = "&quot;"; replen = 6; } break;
402
+ case '\t': if (attr) { rep = "&#9;"; replen = 4; } break;
403
+ case '\n': if (attr) { rep = "&#10;"; replen = 5; } break;
404
+ case '\r': rep = "&#13;"; replen = 5; break;
405
+ default: break;
406
+ }
407
+ if (rep != NULL) {
408
+ if (i > start) MKR_XSER_APPEND(b, s + start, i - start);
409
+ MKR_XSER_APPEND(b, rep, replen);
410
+ start = i + 1;
411
+ }
412
+ }
413
+ if (n > start) MKR_XSER_APPEND(b, s + start, n - start);
414
+ return 0;
415
+ }
416
+
417
+ static int
418
+ mkr_xser_indent(mkr_buf_t *b, int level, int width)
419
+ {
420
+ MKR_XSER_LIT(b, "\n");
421
+ for (int i = 0; i < level * width; i++) MKR_XSER_LIT(b, " ");
422
+ return 0;
423
+ }
424
+
425
+ /* True if any child is character data (TEXT/CDATA): such an element is kept
426
+ * inline even in pretty mode, so its text content is preserved exactly. */
427
+ static int
428
+ mkr_xser_has_chardata(const mkr_xml_node_t *e)
429
+ {
430
+ for (const mkr_xml_node_t *c = e->first_child; c != NULL; c = c->next) {
431
+ if (c->type == MKR_XML_NODE_TYPE_TEXT || c->type == MKR_XML_NODE_TYPE_CDATA_SECTION) {
432
+ return 1;
433
+ }
434
+ }
435
+ return 0;
436
+ }
437
+
438
+ static int mkr_xser_doctype(mkr_buf_t *b, const mkr_xml_node_t *dt);
439
+
440
+ static int
441
+ mkr_xser_node(mkr_buf_t *b, const mkr_xml_node_t *n, int level, int width)
442
+ {
443
+ switch (n->type) {
444
+ case MKR_XML_NODE_TYPE_DOCUMENT_TYPE:
445
+ return mkr_xser_doctype(b, n);
446
+ case MKR_XML_NODE_TYPE_ELEMENT: {
447
+ MKR_XSER_LIT(b, "<");
448
+ MKR_XSER_APPEND(b, n->qname, n->qname_len);
449
+ for (const mkr_xml_node_t *a = n->attrs; a != NULL; a = a->next) {
450
+ MKR_XSER_LIT(b, " ");
451
+ MKR_XSER_APPEND(b, a->qname, a->qname_len);
452
+ MKR_XSER_LIT(b, "=\"");
453
+ if (mkr_xser_escaped(b, a->value ? a->value : "", a->value_len, 1) != 0) return -1;
454
+ MKR_XSER_LIT(b, "\"");
455
+ }
456
+ if (n->first_child == NULL) { MKR_XSER_LIT(b, "/>"); return 0; }
457
+ MKR_XSER_LIT(b, ">");
458
+ int block = width > 0 && !mkr_xser_has_chardata(n);
459
+ for (const mkr_xml_node_t *c = n->first_child; c != NULL; c = c->next) {
460
+ if (block && mkr_xser_indent(b, level + 1, width) != 0) return -1;
461
+ if (mkr_xser_node(b, c, level + 1, width) != 0) return -1;
462
+ }
463
+ if (block && mkr_xser_indent(b, level, width) != 0) return -1;
464
+ MKR_XSER_LIT(b, "</");
465
+ MKR_XSER_APPEND(b, n->qname, n->qname_len);
466
+ MKR_XSER_LIT(b, ">");
467
+ return 0;
468
+ }
469
+ case MKR_XML_NODE_TYPE_TEXT:
470
+ return mkr_xser_escaped(b, n->value ? n->value : "", n->value_len, 0);
471
+ case MKR_XML_NODE_TYPE_CDATA_SECTION:
472
+ MKR_XSER_LIT(b, "<![CDATA[");
473
+ MKR_XSER_APPEND(b, n->value ? n->value : "", n->value_len);
474
+ MKR_XSER_LIT(b, "]]>");
475
+ return 0;
476
+ case MKR_XML_NODE_TYPE_COMMENT:
477
+ MKR_XSER_LIT(b, "<!--");
478
+ MKR_XSER_APPEND(b, n->value ? n->value : "", n->value_len);
479
+ MKR_XSER_LIT(b, "-->");
480
+ return 0;
481
+ case MKR_XML_NODE_TYPE_PI:
482
+ MKR_XSER_LIT(b, "<?");
483
+ MKR_XSER_APPEND(b, n->local, n->local_len);
484
+ if (n->value_len) { MKR_XSER_LIT(b, " "); MKR_XSER_APPEND(b, n->value, n->value_len); }
485
+ MKR_XSER_LIT(b, "?>");
486
+ return 0;
487
+ case MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT:
488
+ /* A fragment has no markup of its own: it serializes as its children, in
489
+ * order, spliced together (the same nodes #add_child would insert). */
490
+ for (const mkr_xml_node_t *c = n->first_child; c != NULL; c = c->next) {
491
+ if (mkr_xser_node(b, c, level, width) != 0) return -1;
492
+ }
493
+ return 0;
494
+ default:
495
+ return 0;
496
+ }
497
+ }
498
+
499
+ /* <!DOCTYPE name [PUBLIC "pub" "sys" | SYSTEM "sys"]> from the off-tree node. */
500
+ static int
501
+ mkr_xser_doctype(mkr_buf_t *b, const mkr_xml_node_t *dt)
502
+ {
503
+ MKR_XSER_LIT(b, "<!DOCTYPE ");
504
+ MKR_XSER_APPEND(b, dt->local, dt->local_len);
505
+ if (dt->prefix != NULL) { /* PUBLIC id present -> "PUBLIC pub sys" */
506
+ MKR_XSER_LIT(b, " PUBLIC \"");
507
+ MKR_XSER_APPEND(b, dt->prefix, dt->prefix_len);
508
+ MKR_XSER_LIT(b, "\" \"");
509
+ MKR_XSER_APPEND(b, dt->value ? dt->value : "", dt->value_len);
510
+ MKR_XSER_LIT(b, "\"");
511
+ } else if (dt->value != NULL) { /* SYSTEM id only */
512
+ MKR_XSER_LIT(b, " SYSTEM \"");
513
+ MKR_XSER_APPEND(b, dt->value, dt->value_len);
514
+ MKR_XSER_LIT(b, "\"");
515
+ }
516
+ MKR_XSER_LIT(b, ">");
517
+ return 0;
518
+ }
519
+
520
+ /* An upper bound for a serialization buffer, scaled to the document's content
521
+ * (its tracked arena_bytes). The serialized form of any acyclic, depth-bounded
522
+ * document is a small multiple of its arena bytes, so 32x - which covers
523
+ * worst-case escaping and maximal pretty-print indentation for a parsed document
524
+ * (depth <= MKR_XML_MAX_DEPTH) - admits every legitimate serialization, with a
525
+ * 64 KiB floor for tiny documents. A cyclic or pathologically deep CONSTRUCTED
526
+ * tree exceeds the bound, so the serializer fails closed with MKR_ERR_LIMIT
527
+ * (-> Makiri::Error) instead of growing the buffer without limit and exhausting
528
+ * memory. Defence-in-depth: the tree-mutation guards already prevent cycles. */
529
+ static size_t
530
+ mkr_xml_serialize_cap(VALUE self)
531
+ {
532
+ mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(mkr_xml_node_document(self)));
533
+ size_t cap = 65536; /* floor: declaration/DOCTYPE + a small subtree */
534
+ size_t arena = xdoc ? xdoc->arena_bytes : 0;
535
+ if (arena > 0) {
536
+ cap = (arena <= (SIZE_MAX - cap) / 32) ? cap + arena * 32 : SIZE_MAX;
537
+ }
538
+ return cap;
539
+ }
540
+
541
+ /* call-seq: node.to_xml(pretty: false, indent: 2, encoding: "UTF-8") -> String
542
+ * A Document also emits the XML declaration and its DOCTYPE; any other node
543
+ * serializes just its own subtree. +encoding+ (a String or Encoding) transcodes
544
+ * the output - a character the target cannot represent becomes a hexadecimal
545
+ * character reference - and is named in a Document's declaration. */
546
+ static VALUE
547
+ mkr_xml_node_to_xml(int argc, VALUE *argv, VALUE self)
548
+ {
549
+ VALUE opts;
550
+ rb_scan_args(argc, argv, "0:", &opts);
551
+ int width = 0;
552
+ VALUE enc_opt = Qnil;
553
+ if (!NIL_P(opts)) {
554
+ if (RTEST(rb_hash_aref(opts, ID2SYM(rb_intern("pretty"))))) width = 2;
555
+ VALUE iv = rb_hash_aref(opts, ID2SYM(rb_intern("indent")));
556
+ if (!NIL_P(iv)) width = NUM2INT(iv) < 0 ? 0 : NUM2INT(iv);
557
+ enc_opt = rb_hash_aref(opts, ID2SYM(rb_intern("encoding")));
558
+ }
559
+ /* resolve the target encoding (raises on an unknown name) + its declared name */
560
+ rb_encoding *to_enc = NIL_P(enc_opt) ? NULL : rb_to_encoding(enc_opt);
561
+ VALUE enc_name = NIL_P(enc_opt) ? Qnil : rb_obj_as_string(enc_opt);
562
+
563
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
564
+ mkr_buf_t buf;
565
+ mkr_buf_init(&buf, mkr_xml_serialize_cap(self)); /* fail closed past the cap, never OOM */
566
+ int rc = 0;
567
+
568
+ if (rb_obj_is_kind_of(self, mkr_cXmlDocument)) {
569
+ mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(self));
570
+ /* Emit the encoding pseudo-attribute only when an explicit encoding: was
571
+ * requested or the parsed source declared one; otherwise a built or
572
+ * declaration-less document round-trips to a bare `<?xml version="1.0"?>`,
573
+ * matching Nokogiri (the output is UTF-8 either way). */
574
+ int emit_enc = !NIL_P(enc_name) || (xdoc != NULL && xdoc->has_encoding_decl);
575
+ if (emit_enc) {
576
+ static const char decl_a[] = "<?xml version=\"1.0\" encoding=\"";
577
+ static const char decl_b[] = "\"?>\n";
578
+ VALUE name = NIL_P(enc_name) ? rb_utf8_str_new_cstr("UTF-8") : enc_name;
579
+ mkr_ruby_borrowed_bytes_t nv = mkr_ruby_bytes_view(name);
580
+ rc = (mkr_buf_append(&buf, decl_a, sizeof(decl_a) - 1) == MKR_OK) ? 0 : -1;
581
+ if (rc == 0) rc = (mkr_buf_append(&buf, nv.ptr, nv.len) == MKR_OK) ? 0 : -1;
582
+ if (rc == 0) rc = (mkr_buf_append(&buf, decl_b, sizeof(decl_b) - 1) == MKR_OK) ? 0 : -1;
583
+ RB_GC_GUARD(nv.value);
584
+ } else {
585
+ static const char decl[] = "<?xml version=\"1.0\"?>\n";
586
+ rc = (mkr_buf_append(&buf, decl, sizeof(decl) - 1) == MKR_OK) ? 0 : -1;
587
+ }
588
+ if (rc == 0 && xdoc != NULL && xdoc->doctype != NULL) {
589
+ rc = mkr_xser_doctype(&buf, xdoc->doctype);
590
+ if (rc == 0) rc = (mkr_buf_append(&buf, "\n", 1) == MKR_OK) ? 0 : -1;
591
+ }
592
+ for (mkr_xml_node_t *c = n->first_child; rc == 0 && c != NULL; c = c->next) {
593
+ rc = mkr_xser_node(&buf, c, 0, width);
594
+ if (rc == 0) rc = (mkr_buf_append(&buf, "\n", 1) == MKR_OK) ? 0 : -1;
595
+ }
596
+ } else {
597
+ rc = mkr_xser_node(&buf, n, 0, width);
598
+ }
599
+
600
+ if (rc != 0) {
601
+ mkr_buf_free(&buf);
602
+ rb_raise(mkr_eError, "failed to serialize XML: output exceeded the size limit or out of memory");
603
+ }
604
+ VALUE str = rb_utf8_str_new(buf.len ? buf.data : "", (long)buf.len);
605
+ mkr_buf_free(&buf);
606
+
607
+ /* Transcode to the requested encoding; an unrepresentable character becomes a
608
+ * &#xNN; reference (ECONV_UNDEF_HEX_CHARREF) rather than raising or dropping. */
609
+ if (to_enc != NULL && to_enc != rb_utf8_encoding() && to_enc != rb_usascii_encoding()) {
610
+ str = rb_str_encode(str, rb_enc_from_encoding(to_enc), ECONV_UNDEF_HEX_CHARREF, Qnil);
611
+ }
612
+ return str;
613
+ }
614
+
615
+ /* ---- Canonical XML 1.0 (#canonicalize) -----------------------------------
616
+ *
617
+ * Inclusive Canonical XML 1.0 (https://www.w3.org/TR/xml-c14n), the form used
618
+ * for XML signatures: UTF-8 output, explicit start/end tags (no `<a/>`),
619
+ * attributes sorted by (namespace-uri, local-name), namespace declarations
620
+ * sorted by prefix with superfluous ones removed, CDATA emitted as escaped text,
621
+ * comments omitted unless requested. Exclusive C14N is not implemented. */
622
+
623
+ /* C14N escaping - text: & < > #xD ; attribute value: & < " #x9 #xA #xD. */
624
+ static int
625
+ mkr_c14n_escaped(mkr_buf_t *b, const char *s, uint32_t n, int attr)
626
+ {
627
+ uint32_t start = 0;
628
+ for (uint32_t i = 0; i < n; i++) {
629
+ const char *rep = NULL;
630
+ size_t replen = 0;
631
+ switch ((unsigned char)s[i]) {
632
+ case '&': rep = "&amp;"; replen = 5; break;
633
+ case '<': rep = "&lt;"; replen = 4; break;
634
+ case '>': if (!attr) { rep = "&gt;"; replen = 4; } break;
635
+ case '"': if (attr) { rep = "&quot;"; replen = 6; } break;
636
+ case 0x09: if (attr) { rep = "&#x9;"; replen = 5; } break;
637
+ case 0x0A: if (attr) { rep = "&#xA;"; replen = 5; } break;
638
+ case 0x0D: rep = "&#xD;"; replen = 5; break;
639
+ default: break;
640
+ }
641
+ if (rep != NULL) {
642
+ if (i > start) MKR_XSER_APPEND(b, s + start, i - start);
643
+ MKR_XSER_APPEND(b, rep, replen);
644
+ start = i + 1;
645
+ }
646
+ }
647
+ if (n > start) MKR_XSER_APPEND(b, s + start, n - start);
648
+ return 0;
649
+ }
650
+
651
+ /* slice compare (lexicographic; shorter sorts first on a shared prefix). */
652
+ static int
653
+ mkr_slice_cmp(const char *a, uint32_t al, const char *bb, uint32_t bl)
654
+ {
655
+ uint32_t m = al < bl ? al : bl;
656
+ int c = m ? memcmp(a ? a : "", bb ? bb : "", m) : 0;
657
+ if (c != 0) return c;
658
+ return al == bl ? 0 : (al < bl ? -1 : 1);
659
+ }
660
+
661
+ static int
662
+ mkr_c14n_attr_cmp(const void *pa, const void *pb) /* by (namespace-uri, local) */
663
+ {
664
+ const mkr_xml_node_t *a = *(const mkr_xml_node_t *const *)pa;
665
+ const mkr_xml_node_t *b = *(const mkr_xml_node_t *const *)pb;
666
+ int c = mkr_slice_cmp(a->ns_uri, a->ns_uri_len, b->ns_uri, b->ns_uri_len);
667
+ return c != 0 ? c : mkr_slice_cmp(a->local, a->local_len, b->local, b->local_len);
668
+ }
669
+
670
+ /* A renderable namespace binding. All pointers are arena slices (stable for the
671
+ * document's lifetime, GC-irrelevant): the C14N walk never builds Ruby objects. */
672
+ typedef struct { const char *prefix; uint32_t plen; const char *uri; uint32_t ulen; } mkr_c14n_ns_t;
673
+
674
+ static int
675
+ mkr_c14n_ns_cmp(const void *pa, const void *pb) /* by prefix (default "" first) */
676
+ {
677
+ const mkr_c14n_ns_t *a = pa, *b = pb;
678
+ return mkr_slice_cmp(a->prefix, a->plen, b->prefix, b->plen);
679
+ }
680
+
681
+
682
+ /* Nearest in-scope binding for +prefix+ at or above +node+ (walks the real tree;
683
+ * no scope dictionary is threaded). */
684
+ static int
685
+ mkr_c14n_nearest(const mkr_xml_node_t *node, const char *prefix, uint32_t plen,
686
+ const char **uri, uint32_t *ulen)
687
+ {
688
+ for (const mkr_xml_node_t *e = node; e != NULL; e = e->parent) {
689
+ if (e->type != MKR_XML_NODE_TYPE_ELEMENT) continue;
690
+ for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
691
+ const char *p, *u; uint32_t pl, ul;
692
+ if (mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)
693
+ && mkr_bytes_eq(p, pl, prefix, plen)) {
694
+ *uri = u; *ulen = ul; return 1;
695
+ }
696
+ }
697
+ }
698
+ return 0;
699
+ }
700
+
701
+ static int
702
+ mkr_c14n_has_prefix(const mkr_c14n_ns_t *arr, size_t n, const char *p, uint32_t pl)
703
+ {
704
+ for (size_t i = 0; i < n; i++)
705
+ if (mkr_bytes_eq(arr[i].prefix, arr[i].plen, p, pl)) return 1;
706
+ return 0;
707
+ }
708
+
709
+ /* The namespace declarations to render at +n+ (Inclusive C14N 1.0). The apex
710
+ * renders every in-scope namespace (walking ancestors, nearest binding winning);
711
+ * a descendant renders only its OWN xmlns declarations that change the inherited
712
+ * binding. Writes a heap array (the caller frees) sorted by prefix; returns the
713
+ * count, or SIZE_MAX on OOM. */
714
+ static size_t
715
+ mkr_c14n_namespaces(const mkr_xml_node_t *n, int is_apex, mkr_c14n_ns_t **out)
716
+ {
717
+ *out = NULL;
718
+ size_t cap = 0;
719
+ for (const mkr_xml_node_t *e = n; e != NULL; e = e->parent) {
720
+ if (e->type == MKR_XML_NODE_TYPE_ELEMENT) {
721
+ for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
722
+ const char *p, *u; uint32_t pl, ul;
723
+ if (mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) cap++;
724
+ }
725
+ }
726
+ if (!is_apex) break; /* a descendant considers only its own declarations */
727
+ }
728
+ if (cap == 0) return 0;
729
+ mkr_c14n_ns_t *arr = mkr_reallocarray(NULL, cap, sizeof(*arr));
730
+ if (arr == NULL) return SIZE_MAX;
731
+ size_t cnt = 0;
732
+ int default_seen = 0;
733
+ for (const mkr_xml_node_t *e = n; e != NULL; e = e->parent) {
734
+ if (e->type == MKR_XML_NODE_TYPE_ELEMENT) {
735
+ for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
736
+ const char *p, *u; uint32_t pl, ul;
737
+ if (!mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) continue;
738
+ if (mkr_bytes_eq(p, pl, "xml", 3)) continue; /* implicit xml: */
739
+ if (is_apex) {
740
+ if (pl == 0) { /* default: nearest decl wins */
741
+ if (default_seen) continue;
742
+ default_seen = 1;
743
+ if (ul == 0) continue; /* nearest default is xmlns="" -> not rendered */
744
+ } else if (mkr_c14n_has_prefix(arr, cnt, p, pl)) {
745
+ continue;
746
+ }
747
+ } else { /* descendant: only changes to the binding */
748
+ const char *au; uint32_t aul;
749
+ int above = mkr_c14n_nearest(e->parent, p, pl, &au, &aul);
750
+ if (above && mkr_bytes_eq(au, aul, u, ul)) continue; /* superfluous */
751
+ if (pl == 0 && ul == 0 && !(above && aul > 0)) continue; /* xmlns="" only undeclares */
752
+ }
753
+ arr[cnt].prefix = p; arr[cnt].plen = pl;
754
+ arr[cnt].uri = u; arr[cnt].ulen = ul;
755
+ cnt++;
756
+ }
757
+ }
758
+ if (!is_apex) break;
759
+ }
760
+ qsort(arr, cnt, sizeof(*arr), mkr_c14n_ns_cmp);
761
+ *out = arr;
762
+ return cnt;
763
+ }
764
+
765
+ static int
766
+ mkr_c14n_node(mkr_buf_t *b, const mkr_xml_node_t *n, int is_apex, int comments)
767
+ {
768
+ switch (n->type) {
769
+ case MKR_XML_NODE_TYPE_ELEMENT: {
770
+ MKR_XSER_LIT(b, "<");
771
+ MKR_XSER_APPEND(b, n->qname, n->qname_len);
772
+
773
+ /* namespace declarations, prefix-sorted (a heap array of arena slices;
774
+ * freed before returning, so no buffer macro may early-return past it). */
775
+ mkr_c14n_ns_t *ns = NULL;
776
+ size_t nn = mkr_c14n_namespaces(n, is_apex, &ns);
777
+ if (nn == (size_t)-1) return -1;
778
+ for (size_t i = 0; i < nn; i++) {
779
+ int ok;
780
+ if (ns[i].plen == 0) {
781
+ ok = mkr_buf_append(b, " xmlns=\"", 8) == MKR_OK;
782
+ } else {
783
+ ok = mkr_buf_append(b, " xmlns:", 7) == MKR_OK
784
+ && mkr_buf_append(b, ns[i].prefix, ns[i].plen) == MKR_OK
785
+ && mkr_buf_append(b, "=\"", 2) == MKR_OK;
786
+ }
787
+ if (ok) ok = mkr_c14n_escaped(b, ns[i].uri, ns[i].ulen, 1) == 0;
788
+ if (ok) ok = mkr_buf_append(b, "\"", 1) == MKR_OK;
789
+ if (!ok) { free(ns); return -1; }
790
+ }
791
+ free(ns);
792
+
793
+ /* attributes (non-xmlns) sorted by (namespace-uri, local-name) */
794
+ size_t na = 0;
795
+ for (mkr_xml_node_t *a = n->attrs; a != NULL; a = a->next) {
796
+ const char *p, *u; uint32_t pl, ul;
797
+ if (!mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) na++;
798
+ }
799
+ if (na > 0) {
800
+ const mkr_xml_node_t **av = mkr_reallocarray(NULL, na, sizeof(*av));
801
+ if (av == NULL) return -1;
802
+ size_t k = 0;
803
+ for (mkr_xml_node_t *a = n->attrs; a != NULL; a = a->next) {
804
+ const char *p, *u; uint32_t pl, ul;
805
+ if (!mkr_xml_node_xmlns_decl(a, &p, &pl, &u, &ul)) av[k++] = a;
806
+ }
807
+ qsort(av, na, sizeof(*av), mkr_c14n_attr_cmp);
808
+ for (size_t i = 0; i < na; i++) {
809
+ int ok = mkr_buf_append(b, " ", 1) == MKR_OK
810
+ && mkr_buf_append(b, av[i]->qname, av[i]->qname_len) == MKR_OK
811
+ && mkr_buf_append(b, "=\"", 2) == MKR_OK;
812
+ if (ok) ok = mkr_c14n_escaped(b, av[i]->value ? av[i]->value : "", av[i]->value_len, 1) == 0;
813
+ if (ok) ok = mkr_buf_append(b, "\"", 1) == MKR_OK;
814
+ if (!ok) { free(av); return -1; }
815
+ }
816
+ free(av);
817
+ }
818
+
819
+ MKR_XSER_LIT(b, ">");
820
+ for (mkr_xml_node_t *c = n->first_child; c != NULL; c = c->next) {
821
+ if (mkr_c14n_node(b, c, 0, comments) != 0) return -1; /* children are not the apex */
822
+ }
823
+ MKR_XSER_LIT(b, "</");
824
+ MKR_XSER_APPEND(b, n->qname, n->qname_len);
825
+ MKR_XSER_LIT(b, ">");
826
+ return 0;
827
+ }
828
+ case MKR_XML_NODE_TYPE_TEXT:
829
+ case MKR_XML_NODE_TYPE_CDATA_SECTION: /* CDATA canonicalizes to escaped text */
830
+ return mkr_c14n_escaped(b, n->value ? n->value : "", n->value_len, 0);
831
+ case MKR_XML_NODE_TYPE_COMMENT:
832
+ if (comments) {
833
+ MKR_XSER_LIT(b, "<!--");
834
+ MKR_XSER_APPEND(b, n->value ? n->value : "", n->value_len);
835
+ MKR_XSER_LIT(b, "-->");
836
+ }
837
+ return 0;
838
+ case MKR_XML_NODE_TYPE_PI:
839
+ MKR_XSER_LIT(b, "<?");
840
+ MKR_XSER_APPEND(b, n->local, n->local_len);
841
+ if (n->value_len) { MKR_XSER_LIT(b, " "); MKR_XSER_APPEND(b, n->value, n->value_len); }
842
+ MKR_XSER_LIT(b, "?>");
843
+ return 0;
844
+ case MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT:
845
+ for (const mkr_xml_node_t *c = n->first_child; c != NULL; c = c->next) {
846
+ if (mkr_c14n_node(b, c, 0, comments) != 0) return -1; /* children are not the apex */
847
+ }
848
+ return 0;
849
+ default:
850
+ return 0;
851
+ }
852
+ }
853
+
854
+ /* call-seq: node.canonicalize(comments: false) -> String
855
+ * Inclusive Canonical XML 1.0 (UTF-8). A Document canonicalizes its element +
856
+ * top-level PIs (and comments when requested); any other node canonicalizes its
857
+ * subtree, with the apex inheriting the ancestors' in-scope namespaces. */
858
+ static VALUE
859
+ mkr_xml_node_canonicalize(int argc, VALUE *argv, VALUE self)
860
+ {
861
+ VALUE opts;
862
+ rb_scan_args(argc, argv, "0:", &opts);
863
+ int comments = !NIL_P(opts) && RTEST(rb_hash_aref(opts, ID2SYM(rb_intern("comments"))));
864
+
865
+ mkr_xml_node_t *n = mkr_xml_node_unwrap(self);
866
+ mkr_buf_t buf;
867
+ mkr_buf_init(&buf, mkr_xml_serialize_cap(self)); /* fail closed past the cap, never OOM */
868
+ int rc = 0;
869
+
870
+ if (rb_obj_is_kind_of(self, mkr_cXmlDocument)) {
871
+ /* §2.4: a PI/comment before the document element is followed by #xA, one
872
+ * after is preceded by #xA; the element itself has no surrounding line. */
873
+ int seen_root = 0;
874
+ for (mkr_xml_node_t *c = n->first_child; rc == 0 && c != NULL; c = c->next) {
875
+ if (c->type == MKR_XML_NODE_TYPE_ELEMENT) {
876
+ rc = mkr_c14n_node(&buf, c, 1, comments); /* the root element is the apex */
877
+ seen_root = 1;
878
+ } else if (c->type == MKR_XML_NODE_TYPE_PI ||
879
+ (c->type == MKR_XML_NODE_TYPE_COMMENT && comments)) {
880
+ if (seen_root && rc == 0) rc = (mkr_buf_append(&buf, "\n", 1) == MKR_OK) ? 0 : -1;
881
+ if (rc == 0) rc = mkr_c14n_node(&buf, c, 0, comments);
882
+ if (!seen_root && rc == 0) rc = (mkr_buf_append(&buf, "\n", 1) == MKR_OK) ? 0 : -1;
883
+ }
884
+ }
885
+ } else {
886
+ rc = mkr_c14n_node(&buf, n, 1, comments); /* the node is the apex (inherits ancestors' ns) */
887
+ }
888
+
889
+ if (rc != 0) {
890
+ mkr_buf_free(&buf);
891
+ rb_raise(mkr_eError, "failed to canonicalize XML: output exceeded the size limit or out of memory");
892
+ }
893
+ VALUE str = rb_utf8_str_new(buf.len ? buf.data : "", (long)buf.len);
894
+ mkr_buf_free(&buf);
895
+ return str;
896
+ }
897
+
898
+ /* ---- fail-closed guard for the unsupported serialization surface ----
899
+ *
900
+ * HTML serialization (to_html/inner_html/outer_html) would silently misbehave on
901
+ * XML (escaping / CDATA / void elements differ), so it is an explicit
902
+ * NotImplementedError rather than a wrong result. Use #to_xml. (CSS selectors,
903
+ * once unsupported here, are now lowered to the native XPath engine - see
904
+ * ruby_xml.c.) */
905
+ static VALUE
906
+ mkr_xml_node_no_serialize(int argc, VALUE *argv, VALUE self)
907
+ {
908
+ (void)argc; (void)argv; (void)self;
909
+ rb_raise(rb_eNotImpError,
910
+ "Makiri::XML does not HTML-serialize (to_html / inner_html / "
911
+ "outer_html); use #to_xml for XML output.");
912
+ }
913
+
914
+ /* ---- node identity (== / eql? / hash / pointer_id) ----
915
+ *
916
+ * XML nodes share the mkr_node_data_t typed-data with HTML nodes, so the
917
+ * underlying node pointer (mkr_node_id, representation-agnostic) IS the identity.
918
+ * The implementations are therefore representation-neutral and live once in
919
+ * ruby_node.c (mkr_node_equals / mkr_node_hash / mkr_node_pointer_id); the XML
920
+ * NodeMethods module just binds to them below, exactly as the HTML module does. */
921
+
922
+ /* ---- mutation (Phase 1: in-place edits) ----------------------------------
923
+ *
924
+ * The write surface over the custom XML arena. The Ruby-free primitives live in
925
+ * xml/mkr_xml_mutate.c (validation, namespace resolution, link/unlink); this
926
+ * layer only coerces+verifies arguments through the bridge and maps the
927
+ * mutation status to a Ruby exception. Detach-never-destroy: a removed node is
928
+ * unlinked, never freed, so live wrappers stay valid (the same invariant the
929
+ * read-only reader had). The XML reader keeps no attr/text index, so - unlike
930
+ * the HTML side - there is nothing to invalidate after an edit. */
931
+
932
+ static mkr_xml_doc_t *
933
+ mkr_xml_node_xdoc(VALUE self)
934
+ {
935
+ return mkr_parsed_xml_doc(mkr_doc_parsed(mkr_xml_node_document(self)));
936
+ }
937
+
938
+ /* A value/name byte length as a uint32 (the arena's per-slice cap), or raise. */
939
+ static uint32_t
940
+ mkr_xml_u32_len(size_t len)
941
+ {
942
+ if (len > UINT32_MAX) {
943
+ rb_raise(mkr_eError, "string too long for an XML node (max 4 GiB)");
944
+ }
945
+ return (uint32_t)len;
946
+ }
947
+
948
+ /* Map a mutation status to a Ruby exception (MKR_XML_MUT_OK returns). */
949
+ static void
950
+ mkr_xml_mut_check(mkr_xml_mut_status_t st)
951
+ {
952
+ switch (st) {
953
+ case MKR_XML_MUT_OK: return;
954
+ case MKR_XML_MUT_OOM: rb_raise(mkr_eError, "out of memory mutating XML");
955
+ case MKR_XML_MUT_BAD_NAME: rb_raise(rb_eArgError, "not a well-formed XML name");
956
+ case MKR_XML_MUT_BAD_CHARS: rb_raise(mkr_eError,
957
+ "value contains a character or sequence not permitted in XML");
958
+ case MKR_XML_MUT_UNBOUND_NS: rb_raise(mkr_eError,
959
+ "namespace prefix is not bound in this scope");
960
+ case MKR_XML_MUT_TYPE: rb_raise(mkr_eError, "operation unsupported for this node type");
961
+ case MKR_XML_MUT_CYCLE: rb_raise(mkr_eError, "cannot insert a node into its own subtree");
962
+ case MKR_XML_MUT_HIERARCHY: rb_raise(mkr_eError,
963
+ "invalid placement (an attribute/document node cannot be a "
964
+ "tree child, a document allows a single root element, and a "
965
+ "sibling target must have a parent)");
966
+ case MKR_XML_MUT_BAD_NS_DECL: rb_raise(mkr_eError,
967
+ "cannot bind a namespace prefix to the empty namespace");
968
+ }
969
+ rb_raise(mkr_eError, "unknown XML mutation error"); /* unreachable; keeps the compiler happy */
970
+ }
971
+
972
+ /* Unwrap an XML node for mutation: a frozen node (Object#freeze) is immutable, so
973
+ * raise FrozenError rather than edit it (the same contract HTML nodes have). */
974
+ static mkr_xml_node_t *
975
+ mkr_xml_node_unwrap_mutable(VALUE self)
976
+ {
977
+ rb_check_frozen(self);
978
+ /* Single mutation choke point (every mutator calls this): drop the cached
979
+ * element-name index so the next query rebuilds it. Same discipline as the
980
+ * HTML attr/text indices, in one place that cannot be forgotten. */
981
+ mkr_xml_name_index_invalidate(mkr_xml_node_xdoc(self));
982
+ return mkr_xml_node_unwrap(self);
983
+ }
984
+
985
+ /* node.remove / node.unlink -> node. Detach from the tree (or, for an attribute,
986
+ * from its owner element); the node stays usable. */
987
+ static VALUE
988
+ mkr_xml_node_remove(VALUE self)
989
+ {
990
+ if (rb_obj_is_kind_of(self, mkr_cXmlDocument)) {
991
+ rb_raise(mkr_eError, "cannot remove the document node");
992
+ }
993
+ mkr_xml_node_t *n = mkr_xml_node_unwrap_mutable(self);
994
+ mkr_xml_doc_t *xdoc = mkr_xml_node_xdoc(self);
995
+ if (xdoc != NULL && n == xdoc->root) xdoc->root = NULL; /* detaching the root element */
996
+ mkr_xml_detach(n);
997
+ return self;
998
+ }
999
+
1000
+ /* element[name] = value -> value. Adds or replaces the attribute. */
1001
+ static VALUE
1002
+ mkr_xml_node_aset(VALUE self, VALUE rb_name, VALUE rb_value)
1003
+ {
1004
+ mkr_xml_node_t *n = mkr_xml_node_unwrap_mutable(self);
1005
+ if (n->type != MKR_XML_NODE_TYPE_ELEMENT) {
1006
+ rb_raise(mkr_eError, "cannot set an attribute on a non-element node");
1007
+ }
1008
+ mkr_ruby_borrowed_text_t nv = mkr_ruby_verified_text(rb_name, "attribute name");
1009
+ mkr_ruby_borrowed_text_t vv = mkr_ruby_verified_text(rb_value, "attribute value");
1010
+ mkr_xml_mut_status_t st = mkr_xml_set_attribute(
1011
+ mkr_xml_node_xdoc(self), n,
1012
+ nv.ptr, mkr_xml_u32_len(nv.len), vv.ptr, mkr_xml_u32_len(vv.len), NULL);
1013
+ RB_GC_GUARD(nv.value);
1014
+ RB_GC_GUARD(vv.value);
1015
+ mkr_xml_mut_check(st);
1016
+ return rb_value;
1017
+ }
1018
+
1019
+ /* element.delete(name) -> self. Removes the attribute if present (no-op otherwise). */
1020
+ static VALUE
1021
+ mkr_xml_node_delete(VALUE self, VALUE rb_name)
1022
+ {
1023
+ mkr_xml_node_t *n = mkr_xml_node_unwrap_mutable(self);
1024
+ if (n->type != MKR_XML_NODE_TYPE_ELEMENT) return self;
1025
+ mkr_ruby_borrowed_text_t nv = mkr_ruby_verified_text(rb_name, "attribute name");
1026
+ mkr_xml_remove_attribute(n, nv.ptr, mkr_xml_u32_len(nv.len));
1027
+ RB_GC_GUARD(nv.value);
1028
+ return self;
1029
+ }
1030
+
1031
+ /* node.content = text -> text. For an element: replace its children with one text
1032
+ * node (the string is stored verbatim and escaped on serialization). For a
1033
+ * text/cdata/comment/PI leaf: set its data. */
1034
+ static VALUE
1035
+ mkr_xml_node_set_content(VALUE self, VALUE rb_text)
1036
+ {
1037
+ mkr_xml_node_t *n = mkr_xml_node_unwrap_mutable(self);
1038
+ mkr_ruby_borrowed_text_t tv = mkr_ruby_verified_text(rb_text, "node content");
1039
+ mkr_xml_mut_status_t st = mkr_xml_set_content(
1040
+ mkr_xml_node_xdoc(self), n, tv.ptr, mkr_xml_u32_len(tv.len));
1041
+ RB_GC_GUARD(tv.value);
1042
+ mkr_xml_mut_check(st);
1043
+ return rb_text;
1044
+ }
1045
+
1046
+ /* node.name = new_name -> new_name. Renames an element or attribute in place
1047
+ * (identity + tree position preserved); the namespace is re-resolved against the
1048
+ * node's in-scope declarations. */
1049
+ static VALUE
1050
+ mkr_xml_node_set_name(VALUE self, VALUE rb_name)
1051
+ {
1052
+ mkr_xml_node_t *n = mkr_xml_node_unwrap_mutable(self);
1053
+ mkr_ruby_borrowed_text_t nv = mkr_ruby_verified_text(rb_name, "node name");
1054
+ mkr_xml_mut_status_t st = mkr_xml_rename(
1055
+ mkr_xml_node_xdoc(self), n, nv.ptr, mkr_xml_u32_len(nv.len));
1056
+ RB_GC_GUARD(nv.value);
1057
+ mkr_xml_mut_check(st);
1058
+ return rb_name;
1059
+ }
1060
+
1061
+ /* ---- Phase 2: building new subtrees --------------------------------------
1062
+ *
1063
+ * Document factories create a detached node in the document's arena; insertion
1064
+ * (add_child / before / after / replace) links a node, resolving the inserted
1065
+ * subtree's namespaces against its new context, and deep-copies (imports) a node
1066
+ * that comes from another document. The Ruby-free primitives live in
1067
+ * xml/mkr_xml_mutate.c. NodeSet / String arguments are a later phase. */
1068
+
1069
+ /* Coerce +arg+ to an XML node that lives in (or is imported into) +xdoc+ (the
1070
+ * target document's arena, whose Ruby VALUE is +target_doc+). A node from another
1071
+ * document is deep-copied; a same-document node is returned as-is (move). */
1072
+ static mkr_xml_node_t *
1073
+ mkr_xml_incoming_node(mkr_xml_doc_t *xdoc, VALUE target_doc, VALUE arg)
1074
+ {
1075
+ if (!rb_obj_is_kind_of(arg, mkr_cNode)
1076
+ || !rb_obj_is_kind_of(mkr_xml_node_document(arg), mkr_cXmlDocument)) {
1077
+ rb_raise(rb_eTypeError,
1078
+ "expected a Makiri::XML node (NodeSet / String arguments are a later phase)");
1079
+ }
1080
+ mkr_xml_node_t *src = mkr_xml_node_unwrap(arg);
1081
+ if (mkr_xml_node_document(arg) == target_doc) {
1082
+ return src; /* same arena -> move */
1083
+ }
1084
+ mkr_xml_node_t *copy = NULL; /* foreign arena -> import a deep copy */
1085
+ mkr_xml_mut_check(mkr_xml_import_subtree(xdoc, src, &copy));
1086
+ return copy;
1087
+ }
1088
+
1089
+ /* The four insertion verbs share this shape: frozen-check self, coerce/import the
1090
+ * argument, run the primitive, and return the inserted node (wrapped from the
1091
+ * target document). +op+ selects the primitive. */
1092
+ typedef enum { MKR_INS_CHILD, MKR_INS_BEFORE, MKR_INS_AFTER, MKR_INS_REPLACE } mkr_ins_op_t;
1093
+
1094
+ /* A DOCUMENT_FRAGMENT contributes its CHILDREN, not itself (like Nokogiri / the
1095
+ * DOM): splice them in place of the fragment, in order, leaving it empty. Each
1096
+ * child is inserted relative to +target+ per +op+ (resolving its namespaces
1097
+ * against the new context, as a single node would); for AFTER the insertion point
1098
+ * advances so the children keep their order, and REPLACE inserts the children
1099
+ * before +target+ then detaches it. Returns the (now empty) fragment, as
1100
+ * Nokogiri's add_child/before/after/replace return the fragment. */
1101
+ static VALUE
1102
+ mkr_xml_splice_fragment(mkr_xml_doc_t *xdoc, mkr_xml_node_t *target,
1103
+ mkr_xml_node_t *frag, VALUE doc_v, mkr_ins_op_t op)
1104
+ {
1105
+ mkr_xml_node_t *ref = target; /* moving insertion point for AFTER */
1106
+ mkr_xml_node_t *c;
1107
+ while ((c = frag->first_child) != NULL) { /* each insert detaches c from frag */
1108
+ mkr_xml_mut_status_t st;
1109
+ switch (op) {
1110
+ case MKR_INS_CHILD: st = mkr_xml_insert_child(xdoc, target, c); break;
1111
+ case MKR_INS_AFTER: st = mkr_xml_insert_after(xdoc, ref, c); ref = c; break;
1112
+ default: st = mkr_xml_insert_before(xdoc, target, c); break; /* BEFORE + REPLACE */
1113
+ }
1114
+ mkr_xml_mut_check(st);
1115
+ }
1116
+ if (op == MKR_INS_REPLACE) {
1117
+ mkr_xml_detach(target); /* drop the replaced node after its children are spliced in */
1118
+ }
1119
+ return mkr_wrap_xml_node(frag, doc_v);
1120
+ }
1121
+
1122
+ static VALUE
1123
+ mkr_xml_node_insert(VALUE self, VALUE arg, mkr_ins_op_t op)
1124
+ {
1125
+ mkr_xml_node_t *target = mkr_xml_node_unwrap_mutable(self);
1126
+ VALUE doc_v = mkr_xml_node_document(self);
1127
+ mkr_xml_doc_t *xdoc = mkr_xml_node_xdoc(self);
1128
+ mkr_xml_node_t *node = mkr_xml_incoming_node(xdoc, doc_v, arg);
1129
+
1130
+ if (node->type == MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT) {
1131
+ return mkr_xml_splice_fragment(xdoc, target, node, doc_v, op);
1132
+ }
1133
+
1134
+ mkr_xml_mut_status_t st;
1135
+ switch (op) {
1136
+ case MKR_INS_CHILD: st = mkr_xml_insert_child(xdoc, target, node); break;
1137
+ case MKR_INS_BEFORE: st = mkr_xml_insert_before(xdoc, target, node); break;
1138
+ case MKR_INS_AFTER: st = mkr_xml_insert_after(xdoc, target, node); break;
1139
+ default: st = mkr_xml_replace_node(xdoc, target, node); break;
1140
+ }
1141
+ mkr_xml_mut_check(st);
1142
+ return mkr_wrap_xml_node(node, doc_v);
1143
+ }
1144
+
1145
+ /* element.add_child(node) -> the inserted node. */
1146
+ static VALUE mkr_xml_node_add_child(VALUE self, VALUE arg) { return mkr_xml_node_insert(self, arg, MKR_INS_CHILD); }
1147
+ /* node.add_previous_sibling(other) / node.before(other) -> the inserted node. */
1148
+ static VALUE mkr_xml_node_before(VALUE self, VALUE arg) { return mkr_xml_node_insert(self, arg, MKR_INS_BEFORE); }
1149
+ /* node.add_next_sibling(other) / node.after(other) -> the inserted node. */
1150
+ static VALUE mkr_xml_node_after(VALUE self, VALUE arg) { return mkr_xml_node_insert(self, arg, MKR_INS_AFTER); }
1151
+ /* node.replace(other) -> the inserted node (the replaced node is detached). */
1152
+ static VALUE mkr_xml_node_replace(VALUE self, VALUE arg) { return mkr_xml_node_insert(self, arg, MKR_INS_REPLACE); }
1153
+
1154
+ /* element << node -> self (Nokogiri's <<: append and return the receiver). */
1155
+ static VALUE
1156
+ mkr_xml_node_lshift(VALUE self, VALUE arg)
1157
+ {
1158
+ mkr_xml_node_insert(self, arg, MKR_INS_CHILD);
1159
+ return self;
1160
+ }
1161
+
1162
+ /* ---- Document factories ---- */
1163
+
1164
+ /* rb_hash_foreach body: set one attribute on the element (arg). Keys/values are
1165
+ * stringified (Nokogiri accepts symbol keys / non-string values), then run
1166
+ * through the normal validated attribute setter. */
1167
+ static int
1168
+ mkr_xml_create_attr_i(VALUE key, VALUE val, VALUE rb_el)
1169
+ {
1170
+ mkr_xml_node_aset(rb_el, rb_obj_as_string(key), rb_obj_as_string(val));
1171
+ return ST_CONTINUE;
1172
+ }
1173
+
1174
+ /* element_children -> NodeSet of the child element nodes (nodeType 1) only, in
1175
+ * document order (the counterpart of HTML's #element_children). */
1176
+ static VALUE
1177
+ mkr_xml_node_element_children(VALUE self)
1178
+ {
1179
+ VALUE doc = mkr_xml_node_document(self);
1180
+ VALUE set = mkr_node_set_new(doc);
1181
+ for (mkr_xml_node_t *c = mkr_xml_node_unwrap(self)->first_child; c != NULL; c = c->next) {
1182
+ if (c->type == MKR_XML_NODE_TYPE_ELEMENT) {
1183
+ mkr_node_set_push(set, (mkr_raw_node_t *)c);
1184
+ }
1185
+ }
1186
+ return set;
1187
+ }
1188
+
1189
+ /* clone_node(deep = false) -> a detached copy of this node in the same document
1190
+ * (element/attribute name case, namespaces, and the CDATA node type preserved);
1191
+ * deep copies the whole subtree. Backs Node#dup / #clone and DOM cloneNode. */
1192
+ static VALUE
1193
+ mkr_xml_node_clone_node(int argc, VALUE *argv, VALUE self)
1194
+ {
1195
+ VALUE rb_deep;
1196
+ rb_scan_args(argc, argv, "01", &rb_deep);
1197
+ mkr_xml_node_t *out = NULL;
1198
+ mkr_xml_mut_check(mkr_xml_clone_node(mkr_xml_node_xdoc(self),
1199
+ mkr_xml_node_unwrap(self),
1200
+ RTEST(rb_deep), &out));
1201
+ return mkr_xml_wrap_rel(self, out);
1202
+ }
1203
+
1204
+ /* create_element(name, content = nil, attributes = {}) -> Element.
1205
+ * Nokogiri-style trailing arguments: a Hash sets attributes, any other (non-nil)
1206
+ * argument is the element's text content. */
1207
+ static VALUE
1208
+ mkr_xml_doc_create_element(int argc, VALUE *argv, VALUE self)
1209
+ {
1210
+ VALUE rb_name, rb_rest;
1211
+ rb_scan_args(argc, argv, "1*", &rb_name, &rb_rest);
1212
+ VALUE rb_content = Qnil, rb_attrs = Qnil;
1213
+ for (long i = 0; i < RARRAY_LEN(rb_rest); i++) {
1214
+ VALUE a = RARRAY_AREF(rb_rest, i);
1215
+ if (RB_TYPE_P(a, T_HASH)) {
1216
+ rb_attrs = a;
1217
+ } else if (!NIL_P(a)) {
1218
+ rb_content = a;
1219
+ }
1220
+ }
1221
+
1222
+ mkr_xml_doc_t *xdoc = mkr_xml_node_xdoc(self);
1223
+ mkr_ruby_borrowed_text_t nv = mkr_ruby_verified_text(rb_name, "element name");
1224
+ mkr_xml_node_t *el = NULL;
1225
+ mkr_xml_mut_status_t st = mkr_xml_new_element(xdoc, nv.ptr, mkr_xml_u32_len(nv.len), &el);
1226
+ RB_GC_GUARD(nv.value);
1227
+ mkr_xml_mut_check(st);
1228
+ if (!NIL_P(rb_content)) {
1229
+ mkr_ruby_borrowed_text_t tv = mkr_ruby_verified_text(rb_content, "element content");
1230
+ st = mkr_xml_set_content(xdoc, el, tv.ptr, mkr_xml_u32_len(tv.len));
1231
+ RB_GC_GUARD(tv.value);
1232
+ mkr_xml_mut_check(st);
1233
+ }
1234
+ VALUE rb_el = mkr_wrap_xml_node(el, self);
1235
+ if (!NIL_P(rb_attrs)) {
1236
+ rb_hash_foreach(rb_attrs, mkr_xml_create_attr_i, rb_el);
1237
+ }
1238
+ return rb_el;
1239
+ }
1240
+
1241
+ /* Shared body for the leaf-data factories (text / comment / cdata). */
1242
+ static VALUE
1243
+ mkr_xml_doc_create_chardata(VALUE self, VALUE rb_text, uint8_t type, const char *what)
1244
+ {
1245
+ mkr_xml_doc_t *xdoc = mkr_xml_node_xdoc(self);
1246
+ mkr_ruby_borrowed_text_t tv = mkr_ruby_verified_text(rb_text, what);
1247
+ mkr_xml_node_t *n = NULL;
1248
+ mkr_xml_mut_status_t st = mkr_xml_new_chardata(xdoc, type, tv.ptr, mkr_xml_u32_len(tv.len), &n);
1249
+ RB_GC_GUARD(tv.value);
1250
+ mkr_xml_mut_check(st);
1251
+ return mkr_wrap_xml_node(n, self);
1252
+ }
1253
+
1254
+ static VALUE mkr_xml_doc_create_text_node(VALUE self, VALUE t) { return mkr_xml_doc_create_chardata(self, t, MKR_XML_NODE_TYPE_TEXT, "text content"); }
1255
+ static VALUE mkr_xml_doc_create_comment(VALUE self, VALUE t) { return mkr_xml_doc_create_chardata(self, t, MKR_XML_NODE_TYPE_COMMENT, "comment content"); }
1256
+ static VALUE mkr_xml_doc_create_cdata(VALUE self, VALUE t) { return mkr_xml_doc_create_chardata(self, t, MKR_XML_NODE_TYPE_CDATA_SECTION, "CDATA content"); }
1257
+
1258
+ static VALUE
1259
+ mkr_xml_doc_create_pi(VALUE self, VALUE rb_target, VALUE rb_data)
1260
+ {
1261
+ mkr_xml_doc_t *xdoc = mkr_xml_node_xdoc(self);
1262
+ mkr_ruby_borrowed_text_t tg = mkr_ruby_verified_text(rb_target, "PI target");
1263
+ mkr_ruby_borrowed_text_t dt = mkr_ruby_verified_text(rb_data, "PI data");
1264
+ mkr_xml_node_t *pi = NULL;
1265
+ mkr_xml_mut_status_t st = mkr_xml_new_pi(
1266
+ xdoc, tg.ptr, mkr_xml_u32_len(tg.len), dt.ptr, mkr_xml_u32_len(dt.len), &pi);
1267
+ RB_GC_GUARD(tg.value);
1268
+ RB_GC_GUARD(dt.value);
1269
+ mkr_xml_mut_check(st);
1270
+ return mkr_wrap_xml_node(pi, self);
1271
+ }
1272
+
1273
+ /* The node-class .new constructors (Element/Text/Comment/CDATASection/ProcessingInstruction.new)
1274
+ * and Document#root= are pure delegations to the document factories / insertion
1275
+ * verbs and live in the Ruby convenience layer (lib/makiri/), so a single
1276
+ * definition serves both HTML and XML and the document-type check is consistent. */
1277
+
1278
+ void
1279
+ mkr_init_xml_node(void)
1280
+ {
1281
+ rb_define_method(mkr_mXmlNodeMethods, "name", mkr_xml_node_name, 0);
1282
+ rb_define_method(mkr_mXmlNodeMethods, "local_name", mkr_xml_node_local_name, 0);
1283
+ rb_define_method(mkr_mXmlNodeMethods, "prefix", mkr_xml_node_prefix, 0);
1284
+ rb_define_method(mkr_mXmlNodeMethods, "namespace_uri", mkr_xml_node_namespace_uri, 0);
1285
+ rb_define_method(mkr_mXmlNodeMethods, "node_type", mkr_xml_node_node_type, 0);
1286
+
1287
+ /* namespace introspection (Nokogiri-compatible) + the Namespace value object */
1288
+ mkr_cXmlNamespace = rb_define_class_under(mkr_mXML, "Namespace", rb_cObject);
1289
+ rb_define_method(mkr_cXmlNamespace, "prefix", mkr_ns_prefix, 0);
1290
+ rb_define_method(mkr_cXmlNamespace, "href", mkr_ns_href, 0);
1291
+ rb_define_method(mkr_cXmlNamespace, "to_s", mkr_ns_href, 0);
1292
+ rb_define_method(mkr_cXmlNamespace, "==", mkr_ns_equal, 1);
1293
+ rb_define_method(mkr_cXmlNamespace, "eql?", mkr_ns_equal, 1);
1294
+ rb_define_method(mkr_cXmlNamespace, "hash", mkr_ns_hash, 0);
1295
+ rb_define_method(mkr_cXmlNamespace, "inspect", mkr_ns_inspect, 0);
1296
+ rb_define_method(mkr_mXmlNodeMethods, "namespace", mkr_xml_node_namespace, 0);
1297
+ rb_define_method(mkr_mXmlNodeMethods, "namespace_definitions", mkr_xml_node_namespace_definitions, 0);
1298
+ rb_define_method(mkr_mXmlNodeMethods, "namespaces", mkr_xml_node_namespaces, 0);
1299
+ rb_define_method(mkr_mXmlNodeMethods, "collect_namespaces", mkr_xml_node_collect_namespaces, 0);
1300
+ rb_define_method(mkr_mXmlNodeMethods, "content", mkr_xml_node_content, 0);
1301
+ rb_define_method(mkr_mXmlNodeMethods, "text", mkr_xml_node_content, 0);
1302
+ rb_define_method(mkr_mXmlNodeMethods, "inner_text", mkr_xml_node_content, 0);
1303
+ rb_define_method(mkr_mXmlNodeMethods, "value", mkr_xml_node_value, 0);
1304
+ rb_define_method(mkr_mXmlNodeMethods, "document", mkr_xml_node_get_document, 0);
1305
+ rb_define_method(mkr_mXmlNodeMethods, "parent", mkr_xml_node_parent, 0);
1306
+ rb_define_method(mkr_mXmlNodeMethods, "next", mkr_xml_node_next, 0);
1307
+ rb_define_method(mkr_mXmlNodeMethods, "next_sibling", mkr_xml_node_next, 0);
1308
+ rb_define_method(mkr_mXmlNodeMethods, "previous", mkr_xml_node_previous, 0);
1309
+ rb_define_method(mkr_mXmlNodeMethods, "previous_sibling", mkr_xml_node_previous, 0);
1310
+ rb_define_method(mkr_mXmlNodeMethods, "child", mkr_xml_node_first_child, 0);
1311
+ rb_define_method(mkr_mXmlNodeMethods, "last_element_child", mkr_xml_node_last_child, 0);
1312
+ rb_define_method(mkr_mXmlNodeMethods, "children", mkr_xml_node_children, 0);
1313
+ rb_define_method(mkr_mXmlNodeMethods, "element_children", mkr_xml_node_element_children, 0);
1314
+ rb_define_method(mkr_mXmlNodeMethods, "clone_node", mkr_xml_node_clone_node, -1);
1315
+ rb_define_method(mkr_mXmlNodeMethods, "[]", mkr_xml_node_aref, 1);
1316
+ rb_define_method(mkr_mXmlNodeMethods, "attribute_nodes", mkr_xml_node_attribute_nodes, 0);
1317
+
1318
+ /* Mutation (Phase 1: in-place edits). Detach-never-destroy; the primitives
1319
+ * live in xml/mkr_xml_mutate.c. */
1320
+ rb_define_method(mkr_mXmlNodeMethods, "remove", mkr_xml_node_remove, 0);
1321
+ rb_define_method(mkr_mXmlNodeMethods, "unlink", mkr_xml_node_remove, 0);
1322
+ rb_define_method(mkr_mXmlNodeMethods, "[]=", mkr_xml_node_aset, 2);
1323
+ rb_define_method(mkr_mXmlNodeMethods, "delete", mkr_xml_node_delete, 1);
1324
+ rb_define_method(mkr_mXmlNodeMethods, "remove_attribute", mkr_xml_node_delete, 1);
1325
+ rb_define_method(mkr_mXmlNodeMethods, "content=", mkr_xml_node_set_content, 1);
1326
+ rb_define_method(mkr_mXmlNodeMethods, "name=", mkr_xml_node_set_name, 1);
1327
+
1328
+ /* Mutation (Phase 2: building). Insertion accepts a single Makiri::XML node;
1329
+ * a node from another document is deep-copied (imported) into this one. */
1330
+ rb_define_method(mkr_mXmlNodeMethods, "add_child", mkr_xml_node_add_child, 1);
1331
+ rb_define_method(mkr_mXmlNodeMethods, "<<", mkr_xml_node_lshift, 1);
1332
+ rb_define_method(mkr_mXmlNodeMethods, "add_previous_sibling", mkr_xml_node_before, 1);
1333
+ rb_define_method(mkr_mXmlNodeMethods, "before", mkr_xml_node_before, 1);
1334
+ rb_define_method(mkr_mXmlNodeMethods, "add_next_sibling", mkr_xml_node_after, 1);
1335
+ rb_define_method(mkr_mXmlNodeMethods, "after", mkr_xml_node_after, 1);
1336
+ rb_define_method(mkr_mXmlNodeMethods, "replace", mkr_xml_node_replace, 1);
1337
+
1338
+ /* Document factories. The node-class .new constructors and Document#root= are
1339
+ * pure delegations to these, defined once in the Ruby layer (lib/makiri/)
1340
+ * so HTML and XML share them. */
1341
+ rb_define_method(mkr_cXmlDocument, "create_element", mkr_xml_doc_create_element, -1);
1342
+ rb_define_method(mkr_cXmlDocument, "create_text_node", mkr_xml_doc_create_text_node, 1);
1343
+ rb_define_method(mkr_cXmlDocument, "create_comment", mkr_xml_doc_create_comment, 1);
1344
+ rb_define_method(mkr_cXmlDocument, "create_cdata", mkr_xml_doc_create_cdata, 1);
1345
+ rb_define_method(mkr_cXmlDocument, "create_cdata_node", mkr_xml_doc_create_cdata, 1);
1346
+ rb_define_method(mkr_cXmlDocument, "create_processing_instruction", mkr_xml_doc_create_pi, 2);
1347
+
1348
+ /* Node identity by underlying pointer, so #path / NodeSet dedup / Set / Hash
1349
+ * work (the same contract HTML nodes have). */
1350
+ rb_define_method(mkr_mXmlNodeMethods, "==", mkr_node_equals, 1);
1351
+ rb_define_method(mkr_mXmlNodeMethods, "eql?", mkr_node_equals, 1);
1352
+ rb_define_method(mkr_mXmlNodeMethods, "hash", mkr_node_hash, 0);
1353
+ rb_define_method(mkr_mXmlNodeMethods, "pointer_id", mkr_node_pointer_id, 0);
1354
+
1355
+ /* XML serialization: #to_xml / #to_s re-emit the subtree (a Document also
1356
+ * emits the declaration + DOCTYPE). Also defined on XML::Document below. */
1357
+ rb_define_method(mkr_mXmlNodeMethods, "to_xml", mkr_xml_node_to_xml, -1);
1358
+ rb_define_method(mkr_mXmlNodeMethods, "to_s", mkr_xml_node_to_xml, -1);
1359
+ rb_define_method(mkr_mXmlNodeMethods, "canonicalize", mkr_xml_node_canonicalize, -1);
1360
+
1361
+ /* CSS selectors (#css / #at_css / #matches?) are supported on XML via the
1362
+ * native XPath engine - registered in ruby_xml.c. Fail-closed: HTML
1363
+ * serialization is unsupported on XML; raise rather than emit wrong markup. */
1364
+ rb_define_method(mkr_mXmlNodeMethods, "to_html", mkr_xml_node_no_serialize, -1);
1365
+ rb_define_method(mkr_mXmlNodeMethods, "inner_html", mkr_xml_node_no_serialize, -1);
1366
+ rb_define_method(mkr_mXmlNodeMethods, "outer_html", mkr_xml_node_no_serialize, -1);
1367
+
1368
+ /* Makiri::XML::DocumentType identifiers (#public_id is the Nokogiri-style
1369
+ * alias of #external_id). #name comes from the shared reader. */
1370
+ rb_define_method(mkr_cXmlDocumentType, "external_id", mkr_xml_dtd_external_id, 0);
1371
+ rb_define_method(mkr_cXmlDocumentType, "public_id", mkr_xml_dtd_external_id, 0);
1372
+ rb_define_method(mkr_cXmlDocumentType, "system_id", mkr_xml_dtd_system_id, 0);
1373
+ }