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.
- checksums.yaml +4 -4
- data/.github/workflows/conformance.yml +22 -0
- data/.github/workflows/libfuzzer.yml +83 -0
- data/.github/workflows/release.yml +12 -7
- data/.github/workflows/security.yml +88 -3
- data/.github/workflows/valgrind.yml +135 -0
- data/CHANGELOG.md +152 -15
- data/README.md +183 -13
- data/Rakefile +294 -7
- data/ext/makiri/bridge/bridge.h +28 -0
- data/ext/makiri/bridge/ruby_string.c +282 -12
- data/ext/makiri/core/mkr_alloc.c +40 -3
- data/ext/makiri/core/mkr_alloc.h +28 -5
- data/ext/makiri/core/mkr_buf.c +47 -3
- data/ext/makiri/core/mkr_buf.h +112 -3
- data/ext/makiri/core/mkr_core.c +143 -0
- data/ext/makiri/core/mkr_core.h +11 -2
- data/ext/makiri/core/mkr_hash.h +1 -1
- data/ext/makiri/core/mkr_span.h +186 -0
- data/ext/makiri/core/mkr_text.h +8 -8
- data/ext/makiri/core/mkr_utf8.c +101 -0
- data/ext/makiri/core/mkr_utf8.h +88 -0
- data/ext/makiri/extconf.rb +123 -10
- data/ext/makiri/fuzz/Makefile +95 -0
- data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
- data/ext/makiri/fuzz/xml_fuzz.c +24 -0
- data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
- data/ext/makiri/glue/glue.h +55 -11
- data/ext/makiri/glue/ruby_doc.c +129 -59
- data/ext/makiri/glue/ruby_html_css.c +292 -0
- data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
- data/ext/makiri/glue/ruby_html_node.c +859 -0
- data/ext/makiri/glue/ruby_html_serialize.c +154 -0
- data/ext/makiri/glue/ruby_node.c +74 -729
- data/ext/makiri/glue/ruby_node_set.c +167 -32
- data/ext/makiri/glue/ruby_xml.c +602 -0
- data/ext/makiri/glue/ruby_xml_node.c +1373 -0
- data/ext/makiri/glue/ruby_xpath.c +63 -30
- data/ext/makiri/glue/ruby_xpath.h +19 -0
- data/ext/makiri/lexbor_compat/compat.h +42 -9
- data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
- data/ext/makiri/lexbor_compat/dom_index.c +2 -2
- data/ext/makiri/lexbor_compat/post_parse.c +100 -10
- data/ext/makiri/lexbor_compat/source_loc.c +15 -13
- data/ext/makiri/lexbor_compat/text_index.c +14 -8
- data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
- data/ext/makiri/makiri.c +184 -6
- data/ext/makiri/makiri.h +43 -2
- data/ext/makiri/xml/mkr_xml.h +125 -0
- data/ext/makiri/xml/mkr_xml_chars.c +195 -0
- data/ext/makiri/xml/mkr_xml_index.c +169 -0
- data/ext/makiri/xml/mkr_xml_index.h +48 -0
- data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
- data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
- data/ext/makiri/xml/mkr_xml_node.c +399 -0
- data/ext/makiri/xml/mkr_xml_node.h +184 -0
- data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
- data/ext/makiri/xpath/mkr_css.c +1023 -0
- data/ext/makiri/xpath/mkr_css.h +65 -0
- data/ext/makiri/xpath/mkr_xpath.c +96 -32
- data/ext/makiri/xpath/mkr_xpath.h +109 -4
- data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
- data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
- data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
- data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
- data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
- data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
- data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
- data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
- data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
- data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
- data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
- data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
- data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
- data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
- data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
- data/lib/makiri/{attribute.rb → attr.rb} +7 -3
- data/lib/makiri/cdata_section.rb +19 -0
- data/lib/makiri/comment.rb +10 -0
- data/lib/makiri/compat_aliases.rb +30 -0
- data/lib/makiri/document.rb +9 -73
- data/lib/makiri/document_fragment.rb +14 -9
- data/lib/makiri/element.rb +4 -4
- data/lib/makiri/html/document.rb +106 -0
- data/lib/makiri/html/node_methods.rb +19 -0
- data/lib/makiri/html.rb +12 -0
- data/lib/makiri/node.rb +58 -15
- data/lib/makiri/node_set.rb +8 -0
- data/lib/makiri/processing_instruction.rb +10 -0
- data/lib/makiri/text.rb +1 -1
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/builder.rb +263 -0
- data/lib/makiri/xml/document.rb +24 -0
- data/lib/makiri/xml/node_methods.rb +84 -0
- data/lib/makiri/xml.rb +10 -0
- data/lib/makiri/xpath_context.rb +1 -1
- data/lib/makiri.rb +24 -5
- data/script/build_native_gem.rb +2 -2
- data/script/check_alloc_failures.rb +266 -0
- data/script/check_c_safety.rb +77 -2
- data/script/check_c_safety_allowlist.yml +102 -0
- data/script/check_leaks.rb +64 -0
- data/script/leaks_harness.rb +64 -0
- data/vendor/lexbor/CMakeLists.txt +6 -0
- data/vendor/lexbor/README.md +12 -0
- data/vendor/lexbor/config.cmake +1 -1
- data/vendor/lexbor/source/lexbor/core/base.h +1 -1
- data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
- data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
- data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
- data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
- data/vendor/lexbor/source/lexbor/html/base.h +1 -1
- data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
- data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
- data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
- data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
- data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
- data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
- data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
- data/vendor/lexbor/source/lexbor/url/base.h +1 -1
- data/vendor/lexbor/source/lexbor/url/url.c +5 -2
- data/vendor/lexbor/source/lexbor/url/url.h +9 -0
- data/vendor/lexbor/version +1 -1
- metadata +53 -9
- data/ext/makiri/glue/ruby_css.c +0 -185
- data/ext/makiri/glue/ruby_serialize.c +0 -92
- data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
- 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 = "&"; replen = 5; break;
|
|
399
|
+
case '<': rep = "<"; replen = 4; break;
|
|
400
|
+
case '>': rep = ">"; replen = 4; break;
|
|
401
|
+
case '"': if (attr) { rep = """; replen = 6; } break;
|
|
402
|
+
case '\t': if (attr) { rep = "	"; replen = 4; } break;
|
|
403
|
+
case '\n': if (attr) { rep = " "; replen = 5; } break;
|
|
404
|
+
case '\r': rep = " "; 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 = "&"; replen = 5; break;
|
|
633
|
+
case '<': rep = "<"; replen = 4; break;
|
|
634
|
+
case '>': if (!attr) { rep = ">"; replen = 4; } break;
|
|
635
|
+
case '"': if (attr) { rep = """; replen = 6; } break;
|
|
636
|
+
case 0x09: if (attr) { rep = "	"; replen = 5; } break;
|
|
637
|
+
case 0x0A: if (attr) { rep = "
"; replen = 5; } break;
|
|
638
|
+
case 0x0D: rep = "
"; 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, ©));
|
|
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
|
+
}
|