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,602 @@
|
|
|
1
|
+
/* ruby_xml.c - Ruby boundary for the native XML reader (Phase 1).
|
|
2
|
+
*
|
|
3
|
+
* Makiri::XML::Document.parse(source) (and the Makiri::XML(source) convenience
|
|
4
|
+
* that delegates to it): strict-decode the input (§2.1), then run the Ruby-free
|
|
5
|
+
* parser with the GVL released, and return a
|
|
6
|
+
* Makiri::XML::Document. The document is held in a kind=MKR_DOC_XML mkr_parsed_t
|
|
7
|
+
* (the common document handle, §2.3) and wrapped by mkr_wrap_document, which GC
|
|
8
|
+
* frees via mkr_parsed_destroy (the XML branch whole-arena-frees).
|
|
9
|
+
*/
|
|
10
|
+
#include "../makiri.h"
|
|
11
|
+
#include "../core/mkr_core.h"
|
|
12
|
+
#include "../xml/mkr_xml.h"
|
|
13
|
+
#include "../xml/mkr_xml_node.h"
|
|
14
|
+
#include "glue.h" /* mkr_wrap_document, mkr_parsed_* (via compat.h) */
|
|
15
|
+
#include "ruby_xpath.h" /* mkr_xpath_value_to_ruby / mkr_xpath_raise (shared) */
|
|
16
|
+
#include "../xpath/mkr_xpath.h"
|
|
17
|
+
#include "../xpath/mkr_css.h" /* mkr_css_compile - CSS selectors over XML via the XPath engine */
|
|
18
|
+
#include "../xml/mkr_xml_index.h" /* element-name index for the //name fast path */
|
|
19
|
+
|
|
20
|
+
#include <ruby/thread.h>
|
|
21
|
+
|
|
22
|
+
/* void-typed adapters so the engine's representation-neutral name-index hooks
|
|
23
|
+
* can reach the XML element-name index (mkr_xml_index.c) without the engine
|
|
24
|
+
* knowing its concrete types. */
|
|
25
|
+
static void *
|
|
26
|
+
mkr_xml_name_index_get_v(void *owner)
|
|
27
|
+
{
|
|
28
|
+
return mkr_xml_name_index_get((mkr_xml_doc_t *)owner);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static void *const *
|
|
32
|
+
mkr_xml_name_index_lookup_v(const void *idx, const char *local, size_t local_len,
|
|
33
|
+
const char *ns_uri, size_t ns_uri_len, size_t *count)
|
|
34
|
+
{
|
|
35
|
+
return (void *const *)mkr_xml_name_index_lookup(
|
|
36
|
+
(const mkr_xml_name_index_t *)idx, local, local_len, ns_uri, ns_uri_len, count);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Install the document's element-name index hooks on +ctx+ (the engine lazily
|
|
40
|
+
* builds it only when a document-rooted descendant name test runs). */
|
|
41
|
+
static void
|
|
42
|
+
mkr_xml_install_name_index(mkr_xpath_context_t *ctx, mkr_xml_doc_t *xdoc)
|
|
43
|
+
{
|
|
44
|
+
mkr_xpath_context_set_name_index(ctx, (void *)xdoc,
|
|
45
|
+
mkr_xml_name_index_get_v,
|
|
46
|
+
mkr_xml_name_index_lookup_v);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ---- GVL-released parse ---- */
|
|
50
|
+
typedef struct {
|
|
51
|
+
const char *src;
|
|
52
|
+
size_t len;
|
|
53
|
+
mkr_xml_limits_t limits; /* per-parse budget overrides (0 fields = default) */
|
|
54
|
+
mkr_xml_doc_t *result;
|
|
55
|
+
mkr_xml_status_t status;
|
|
56
|
+
} mkr_xml_parse_nogvl_t;
|
|
57
|
+
|
|
58
|
+
static void *
|
|
59
|
+
mkr_xml_parse_nogvl(void *p)
|
|
60
|
+
{
|
|
61
|
+
mkr_xml_parse_nogvl_t *a = (mkr_xml_parse_nogvl_t *)p;
|
|
62
|
+
a->result = mkr_xml_parse_ex(a->src, a->len, &a->limits, &a->status);
|
|
63
|
+
return NULL;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Read the optional per-parse budget overrides from the keyword hash. Today only
|
|
67
|
+
* max_bytes (the arena memory ceiling) is configurable; rb_get_kwargs rejects any
|
|
68
|
+
* other keyword, and new budgets join the table here as they become runtime-
|
|
69
|
+
* configurable. max_bytes must be a positive Integer (0 / negative / non-Integer
|
|
70
|
+
* raise), and 0 in the struct means "use the compile-time default". */
|
|
71
|
+
static mkr_xml_limits_t
|
|
72
|
+
mkr_xml_parse_limits(VALUE rb_opts)
|
|
73
|
+
{
|
|
74
|
+
mkr_xml_limits_t limits = { 0 };
|
|
75
|
+
if (NIL_P(rb_opts)) return limits;
|
|
76
|
+
|
|
77
|
+
static ID kw_ids[1];
|
|
78
|
+
if (kw_ids[0] == 0) kw_ids[0] = rb_intern_const("max_bytes");
|
|
79
|
+
VALUE kw_vals[1];
|
|
80
|
+
rb_get_kwargs(rb_opts, kw_ids, 0, 1, kw_vals); /* unknown keyword -> ArgumentError */
|
|
81
|
+
|
|
82
|
+
if (kw_vals[0] != Qundef) {
|
|
83
|
+
if (!RB_INTEGER_TYPE_P(kw_vals[0])) {
|
|
84
|
+
rb_raise(rb_eTypeError, "max_bytes must be an Integer");
|
|
85
|
+
}
|
|
86
|
+
/* Reject <= 0 BEFORE the unsigned conversion: NUM2SIZET wraps a negative
|
|
87
|
+
* Integer into a huge size_t (an accidental budget bypass), so guard the
|
|
88
|
+
* sign first. A too-large positive still raises RangeError in NUM2SIZET. */
|
|
89
|
+
if (RTEST(rb_funcall(kw_vals[0], rb_intern("<="), 1, INT2FIX(0)))) {
|
|
90
|
+
rb_raise(rb_eArgError, "max_bytes must be positive");
|
|
91
|
+
}
|
|
92
|
+
limits.max_bytes = NUM2SIZET(kw_vals[0]);
|
|
93
|
+
}
|
|
94
|
+
return limits;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* call-seq: Makiri::XML::Document.parse(source, max_bytes: nil) -> Makiri::XML::Document
|
|
98
|
+
* Makiri::XML(source, max_bytes: nil) -> Makiri::XML::Document
|
|
99
|
+
* +source+ is a String or any object responding to +#read+ (an IO / File /
|
|
100
|
+
* StringIO); +max_bytes+ overrides the default arena memory ceiling for this
|
|
101
|
+
* parse. Read a non-UTF-8 file in binary mode (File.binread / "rb") so the
|
|
102
|
+
* encoding is autodetected from its BOM / declaration. */
|
|
103
|
+
static VALUE
|
|
104
|
+
mkr_xml_s_parse(int argc, VALUE *argv, VALUE self)
|
|
105
|
+
{
|
|
106
|
+
(void)self;
|
|
107
|
+
VALUE rb_source, rb_opts;
|
|
108
|
+
rb_scan_args(argc, argv, "1:", &rb_source, &rb_opts);
|
|
109
|
+
mkr_xml_limits_t limits = mkr_xml_parse_limits(rb_opts); /* validates; may raise */
|
|
110
|
+
size_t budget = limits.max_bytes ? limits.max_bytes : (size_t)MKR_XML_MAX_BYTES;
|
|
111
|
+
|
|
112
|
+
/* Read an IO/File-like source (an object responding to #read), like the HTML
|
|
113
|
+
* entry; a String passes straight through. */
|
|
114
|
+
if (rb_respond_to(rb_source, rb_intern("read"))) {
|
|
115
|
+
rb_source = rb_funcall(rb_source, rb_intern("read"), 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Strict decode under the GVL: invalid UTF-8 / undecodable byte / NUL all
|
|
119
|
+
* raise Makiri::XML::SyntaxError here (no U+FFFD repair). Passing the budget
|
|
120
|
+
* lets decode reject an over-budget input (LimitExceeded) before its
|
|
121
|
+
* validation copy and the GVL-release copy below - so a hostile oversized
|
|
122
|
+
* document is not materialised twice for a doomed parse. */
|
|
123
|
+
VALUE decoded = mkr_xml_decode_input(rb_String(rb_source), budget);
|
|
124
|
+
|
|
125
|
+
/* Copy the decoded bytes into a private C buffer up front - BEFORE allocating
|
|
126
|
+
* any Ruby object (the wrap below) - so there is NO GC point between obtaining
|
|
127
|
+
* +decoded+ and copying it, and the parse can then run with the GVL released
|
|
128
|
+
* without racing GC/compaction on the String's backing store. */
|
|
129
|
+
mkr_owned_bytes_t source = {0};
|
|
130
|
+
if (mkr_ruby_copy_bytes(decoded, &source) != 0) {
|
|
131
|
+
rb_raise(mkr_eError, "out of memory copying XML source");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Build an empty XML handle and wrap it (doc == NULL) so a failure mid-parse
|
|
135
|
+
* frees cleanly via GC (mkr_parsed_destroy -> the XML branch ->
|
|
136
|
+
* mkr_xml_doc_destroy(NULL), a no-op). The source is already copied, so this
|
|
137
|
+
* Ruby allocation cannot disturb it. */
|
|
138
|
+
mkr_parsed_t *parsed = mkr_parsed_new_xml(NULL);
|
|
139
|
+
if (parsed == NULL) {
|
|
140
|
+
mkr_owned_bytes_clear(&source);
|
|
141
|
+
rb_raise(mkr_eError, "out of memory allocating XML document");
|
|
142
|
+
}
|
|
143
|
+
VALUE obj = mkr_wrap_document(parsed); /* GC owns +parsed+ from here */
|
|
144
|
+
|
|
145
|
+
mkr_xml_parse_nogvl_t args = { source.ptr, source.len, limits, NULL, MKR_XML_OK };
|
|
146
|
+
rb_thread_call_without_gvl(mkr_xml_parse_nogvl, &args, NULL, NULL);
|
|
147
|
+
mkr_owned_bytes_clear(&source);
|
|
148
|
+
|
|
149
|
+
if (args.result == NULL) {
|
|
150
|
+
switch (args.status) {
|
|
151
|
+
case MKR_XML_ERR_SYNTAX: rb_raise(mkr_eXmlSyntaxError, "malformed XML"); break;
|
|
152
|
+
case MKR_XML_ERR_LIMIT: rb_raise(mkr_eXmlLimitExceeded, "XML document budget exceeded"); break;
|
|
153
|
+
case MKR_XML_ERR_VERSION: rb_raise(mkr_eXmlSyntaxError,
|
|
154
|
+
"unsupported XML version (only XML 1.0 is supported)"); break;
|
|
155
|
+
default: rb_raise(mkr_eError, "failed to parse XML document"); break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
mkr_parsed_set_xml_doc(parsed, args.result);
|
|
159
|
+
return obj;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* XPath value -> Ruby and error -> exception are shared with the HTML query glue
|
|
163
|
+
* (mkr_xpath_value_to_ruby / mkr_xpath_raise, ruby_xpath.h): both query entry
|
|
164
|
+
* points run the same engine and return the same public value/error types, so
|
|
165
|
+
* the conversion lives in one place. mkr_node_set_new/push are kind-aware, so the
|
|
166
|
+
* shared value converter wraps these results as Makiri::XML::* nodes. */
|
|
167
|
+
|
|
168
|
+
/* Resolve the (document VALUE, context node) an XPath query runs against: for a
|
|
169
|
+
* Makiri::XML::Document the context is the document node, for a node it is that
|
|
170
|
+
* node (and its owning Document). */
|
|
171
|
+
static mkr_xml_node_t *
|
|
172
|
+
mkr_xml_query_context(VALUE self, VALUE *out_document)
|
|
173
|
+
{
|
|
174
|
+
/* mkr_xml_node_unwrap is kind-checked (raises on a non-XML node) and resolves
|
|
175
|
+
* an XML Document to its arena document node; mkr_node_document gives the
|
|
176
|
+
* keepalive Document for either. */
|
|
177
|
+
*out_document = mkr_node_document(self);
|
|
178
|
+
return mkr_xml_node_unwrap(self);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Register a {prefix => uri} Ruby Hash onto +ctx+ for a single query. On any bad
|
|
182
|
+
* entry (non-string-coercible, invalid UTF-8 / embedded NUL, or OOM) the context
|
|
183
|
+
* is freed and an exception is raised - never a partial registration. RSS/Atom
|
|
184
|
+
* live in a default namespace, so a prefix is the strict-mode way to select them
|
|
185
|
+
* (e.g. xpath("//a:entry", "a" => "http://www.w3.org/2005/Atom")). */
|
|
186
|
+
static void
|
|
187
|
+
mkr_xml_register_query_namespaces(mkr_xpath_context_t *ctx, VALUE rb_ns)
|
|
188
|
+
{
|
|
189
|
+
if (NIL_P(rb_ns)) return;
|
|
190
|
+
if (!RB_TYPE_P(rb_ns, T_HASH)) {
|
|
191
|
+
mkr_xpath_context_free(ctx);
|
|
192
|
+
rb_raise(rb_eTypeError, "namespaces must be a Hash of prefix => uri");
|
|
193
|
+
}
|
|
194
|
+
size_t cap = mkr_ctx_limits(ctx)->max_string_bytes;
|
|
195
|
+
VALUE keys = rb_funcall(rb_ns, rb_intern("keys"), 0);
|
|
196
|
+
for (long i = 0; i < RARRAY_LEN(keys); i++) {
|
|
197
|
+
VALUE k = rb_ary_entry(keys, i);
|
|
198
|
+
VALUE ks = rb_obj_as_string(k);
|
|
199
|
+
VALUE vs = rb_obj_as_string(rb_hash_aref(rb_ns, k));
|
|
200
|
+
mkr_ruby_borrowed_text_t pv, uv;
|
|
201
|
+
const char *bad = mkr_ruby_try_verified_text(ks, cap, &pv);
|
|
202
|
+
if (bad == NULL) bad = mkr_ruby_try_verified_text(vs, cap, &uv);
|
|
203
|
+
if (bad != NULL) {
|
|
204
|
+
mkr_xpath_context_free(ctx);
|
|
205
|
+
rb_raise(mkr_eError, "invalid namespace mapping: %s", bad);
|
|
206
|
+
}
|
|
207
|
+
int rc = mkr_xpath_register_ns(ctx, mkr_verified_text_from_view(pv),
|
|
208
|
+
mkr_verified_text_from_view(uv));
|
|
209
|
+
RB_GC_GUARD(ks);
|
|
210
|
+
RB_GC_GUARD(vs);
|
|
211
|
+
if (rc != 0) {
|
|
212
|
+
mkr_xpath_context_free(ctx);
|
|
213
|
+
rb_raise(mkr_eError, "failed to register namespace");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Makiri::XML::{Document,*}#xpath(expr, namespaces = nil) / #at_xpath(...):
|
|
219
|
+
* evaluate +expr+ over the XML engine instance, rooted at +self+'s context node,
|
|
220
|
+
* and return a NodeSet (node-set) or scalar. +namespaces+ is an optional
|
|
221
|
+
* {prefix => uri} Hash registered for this query (RSS/Atom default-namespace
|
|
222
|
+
* docs need a prefix under strict matching). Phase 1: no custom-function
|
|
223
|
+
* handler. Makiri::XPathContext is the alternative when many queries share one
|
|
224
|
+
* namespace set (it caches the registrations and the compiled ASTs). */
|
|
225
|
+
static VALUE
|
|
226
|
+
mkr_xml_doc_xpath_run(VALUE self, VALUE rb_expr, VALUE rb_ns, int first_only)
|
|
227
|
+
{
|
|
228
|
+
VALUE document = Qnil;
|
|
229
|
+
mkr_xml_node_t *context = mkr_xml_query_context(self, &document);
|
|
230
|
+
if (context == NULL) {
|
|
231
|
+
return first_only ? Qnil : mkr_node_set_new(document);
|
|
232
|
+
}
|
|
233
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(document));
|
|
234
|
+
|
|
235
|
+
/* The document node is the "document" (the engine's XML namespace services
|
|
236
|
+
* ignore it) and the "/" root for absolute paths; +context+ is the relative
|
|
237
|
+
* context node (the document node for a Document, else the node itself). */
|
|
238
|
+
mkr_xpath_context_t *ctx =
|
|
239
|
+
mkr_xpath_context_new((void *)xdoc->doc_node, (void *)context);
|
|
240
|
+
if (ctx == NULL) {
|
|
241
|
+
rb_raise(mkr_eError, "failed to allocate XPath context");
|
|
242
|
+
}
|
|
243
|
+
mkr_xpath_set_engine_kind(ctx, 1);
|
|
244
|
+
mkr_xml_install_name_index(ctx, xdoc);
|
|
245
|
+
mkr_xml_register_query_namespaces(ctx, rb_ns); /* frees ctx + raises on error */
|
|
246
|
+
|
|
247
|
+
/* Mint the borrowed expression view AFTER namespace registration: that step
|
|
248
|
+
* allocates Ruby objects (and may run GC), and the borrowed bytes must not
|
|
249
|
+
* be held live across it. mkr_parse below runs pure C. */
|
|
250
|
+
mkr_ruby_borrowed_text_t ev = mkr_ruby_verified_text(rb_expr, "XPath expression");
|
|
251
|
+
mkr_xpath_error_t error = {0};
|
|
252
|
+
mkr_xpath_limits_t *limits = mkr_ctx_limits(ctx);
|
|
253
|
+
limits->ast_nodes = 0;
|
|
254
|
+
mkr_node_t *ast = mkr_parse(mkr_verified_text_from_view(ev), limits, &error);
|
|
255
|
+
RB_GC_GUARD(ev.value);
|
|
256
|
+
if (ast == NULL) {
|
|
257
|
+
mkr_xpath_context_free(ctx);
|
|
258
|
+
mkr_xpath_raise(&error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
mkr_xpath_value_t value = {0};
|
|
262
|
+
int rc = first_only ? mkr_xpath_eval_compiled_first(ctx, ast, &value, &error)
|
|
263
|
+
: mkr_xpath_eval_compiled(ctx, ast, &value, &error);
|
|
264
|
+
mkr_node_free(ast);
|
|
265
|
+
if (rc != 0) {
|
|
266
|
+
mkr_xpath_context_free(ctx);
|
|
267
|
+
mkr_xpath_raise(&error);
|
|
268
|
+
}
|
|
269
|
+
VALUE result = mkr_xpath_value_to_ruby(&value, document); /* converts AND clears value */
|
|
270
|
+
mkr_xpath_context_free(ctx);
|
|
271
|
+
|
|
272
|
+
if (first_only && rb_obj_is_kind_of(result, mkr_cNodeSet)) {
|
|
273
|
+
return rb_funcall(result, rb_intern("first"), 0);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static VALUE
|
|
279
|
+
mkr_xml_doc_xpath(int argc, VALUE *argv, VALUE self)
|
|
280
|
+
{
|
|
281
|
+
VALUE expr, ns;
|
|
282
|
+
rb_scan_args(argc, argv, "11", &expr, &ns);
|
|
283
|
+
return mkr_xml_doc_xpath_run(self, expr, ns, 0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static VALUE
|
|
287
|
+
mkr_xml_doc_at_xpath(int argc, VALUE *argv, VALUE self)
|
|
288
|
+
{
|
|
289
|
+
VALUE expr, ns;
|
|
290
|
+
rb_scan_args(argc, argv, "11", &expr, &ns);
|
|
291
|
+
return mkr_xml_doc_xpath_run(self, expr, ns, 1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ---- CSS selectors over XML (lowered to the native XPath engine) ----
|
|
295
|
+
*
|
|
296
|
+
* Makiri::XML CSS support compiles a selector to the engine's AST (mkr_css.c)
|
|
297
|
+
* and runs it through the SAME evaluator as #xpath, so case-sensitivity,
|
|
298
|
+
* namespaces, budgets and document order are identical. The Ruby wrappers
|
|
299
|
+
* (lib/makiri/xml/node_methods.rb) collect the document's namespaces and pass a
|
|
300
|
+
* normalised {prefix => uri} hash; the default namespace, if any, arrives under
|
|
301
|
+
* the synthetic prefix "xmlns" (Nokogiri's convention), and a bare type selector
|
|
302
|
+
* binds to it. These are the private `_css` / `_at_css` / `_css_matches?`
|
|
303
|
+
* primitives those wrappers call. */
|
|
304
|
+
|
|
305
|
+
/* The synthetic default-namespace prefix, present iff the (already
|
|
306
|
+
* prefix-normalised) namespace hash carries an "xmlns" key. */
|
|
307
|
+
static const char *
|
|
308
|
+
mkr_css_default_prefix(VALUE rb_ns)
|
|
309
|
+
{
|
|
310
|
+
if (RB_TYPE_P(rb_ns, T_HASH)
|
|
311
|
+
&& !NIL_P(rb_hash_aref(rb_ns, rb_str_new_cstr(MKR_CSS_DEFAULT_NS_PREFIX)))) {
|
|
312
|
+
return MKR_CSS_DEFAULT_NS_PREFIX;
|
|
313
|
+
}
|
|
314
|
+
return NULL;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Compile +rb_selector+ to an AST under +ctx+ (its namespaces already
|
|
318
|
+
* registered). On a CSS syntax error raises Makiri::CSS::SyntaxError; on any
|
|
319
|
+
* other engine error frees ctx and raises via the shared raiser. Returns the AST
|
|
320
|
+
* (caller frees with mkr_node_free). */
|
|
321
|
+
static mkr_node_t *
|
|
322
|
+
mkr_css_compile_or_raise(mkr_xpath_context_t *ctx, VALUE rb_selector, VALUE rb_ns)
|
|
323
|
+
{
|
|
324
|
+
mkr_css_ns_t cns = { mkr_css_default_prefix(rb_ns) };
|
|
325
|
+
mkr_ruby_borrowed_text_t sv = mkr_ruby_verified_text(rb_selector, "CSS selector");
|
|
326
|
+
mkr_xpath_error_t error = {0};
|
|
327
|
+
mkr_xpath_limits_t *limits = mkr_ctx_limits(ctx);
|
|
328
|
+
limits->ast_nodes = 0;
|
|
329
|
+
mkr_node_t *ast = mkr_css_compile(mkr_verified_text_from_view(sv), &cns, limits, &error);
|
|
330
|
+
RB_GC_GUARD(sv.value);
|
|
331
|
+
if (ast == NULL) {
|
|
332
|
+
int syntax = (error.status == MKR_XPATH_ERR_SYNTAX);
|
|
333
|
+
mkr_xpath_context_free(ctx);
|
|
334
|
+
if (syntax) {
|
|
335
|
+
VALUE msg = error.message ? rb_utf8_str_new_cstr(error.message)
|
|
336
|
+
: rb_str_new_cstr("invalid CSS selector");
|
|
337
|
+
mkr_xpath_error_clear(&error);
|
|
338
|
+
rb_raise(mkr_eCSSSyntaxError, "%" PRIsVALUE, msg);
|
|
339
|
+
}
|
|
340
|
+
mkr_xpath_raise(&error); /* frees error.message, never returns */
|
|
341
|
+
}
|
|
342
|
+
return ast;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static VALUE
|
|
346
|
+
mkr_xml_doc_css_run(VALUE self, VALUE rb_selector, VALUE rb_ns, int first_only)
|
|
347
|
+
{
|
|
348
|
+
VALUE document = Qnil;
|
|
349
|
+
mkr_xml_node_t *context = mkr_xml_query_context(self, &document);
|
|
350
|
+
if (context == NULL) {
|
|
351
|
+
return first_only ? Qnil : mkr_node_set_new(document);
|
|
352
|
+
}
|
|
353
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(document));
|
|
354
|
+
|
|
355
|
+
mkr_xpath_context_t *ctx =
|
|
356
|
+
mkr_xpath_context_new((void *)xdoc->doc_node, (void *)context);
|
|
357
|
+
if (ctx == NULL) {
|
|
358
|
+
rb_raise(mkr_eError, "failed to allocate XPath context");
|
|
359
|
+
}
|
|
360
|
+
mkr_xpath_set_engine_kind(ctx, 1);
|
|
361
|
+
mkr_xml_install_name_index(ctx, xdoc);
|
|
362
|
+
mkr_xml_register_query_namespaces(ctx, rb_ns); /* frees ctx + raises on error */
|
|
363
|
+
|
|
364
|
+
mkr_node_t *ast = mkr_css_compile_or_raise(ctx, rb_selector, rb_ns); /* frees ctx + raises on error */
|
|
365
|
+
|
|
366
|
+
mkr_xpath_value_t value = {0};
|
|
367
|
+
mkr_xpath_error_t error = {0};
|
|
368
|
+
int rc = first_only ? mkr_xpath_eval_compiled_first(ctx, ast, &value, &error)
|
|
369
|
+
: mkr_xpath_eval_compiled(ctx, ast, &value, &error);
|
|
370
|
+
mkr_node_free(ast);
|
|
371
|
+
if (rc != 0) {
|
|
372
|
+
mkr_xpath_context_free(ctx);
|
|
373
|
+
mkr_xpath_raise(&error);
|
|
374
|
+
}
|
|
375
|
+
VALUE result = mkr_xpath_value_to_ruby(&value, document); /* converts AND clears value */
|
|
376
|
+
mkr_xpath_context_free(ctx);
|
|
377
|
+
|
|
378
|
+
if (first_only && rb_obj_is_kind_of(result, mkr_cNodeSet)) {
|
|
379
|
+
return rb_funcall(result, rb_intern("first"), 0);
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static VALUE
|
|
385
|
+
mkr_xml_node_css(VALUE self, VALUE selector, VALUE ns)
|
|
386
|
+
{
|
|
387
|
+
return mkr_xml_doc_css_run(self, selector, ns, 0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
static VALUE
|
|
391
|
+
mkr_xml_node_at_css(VALUE self, VALUE selector, VALUE ns)
|
|
392
|
+
{
|
|
393
|
+
return mkr_xml_doc_css_run(self, selector, ns, 1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* #matches?(selector): does THIS node match the selector? Evaluated by selecting
|
|
397
|
+
* every matching node in the whole document (context = the document node, so the
|
|
398
|
+
* descendant-rooted selector scans the entire tree) and testing membership by
|
|
399
|
+
* node identity - the full-combinator-correct semantics. */
|
|
400
|
+
static VALUE
|
|
401
|
+
mkr_xml_node_css_matches(VALUE self, VALUE selector, VALUE ns)
|
|
402
|
+
{
|
|
403
|
+
VALUE document = Qnil;
|
|
404
|
+
mkr_xml_node_t *node = mkr_xml_query_context(self, &document);
|
|
405
|
+
if (node == NULL) {
|
|
406
|
+
return Qfalse;
|
|
407
|
+
}
|
|
408
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(document));
|
|
409
|
+
|
|
410
|
+
mkr_xpath_context_t *ctx =
|
|
411
|
+
mkr_xpath_context_new((void *)xdoc->doc_node, (void *)xdoc->doc_node);
|
|
412
|
+
if (ctx == NULL) {
|
|
413
|
+
rb_raise(mkr_eError, "failed to allocate XPath context");
|
|
414
|
+
}
|
|
415
|
+
mkr_xpath_set_engine_kind(ctx, 1);
|
|
416
|
+
mkr_xml_install_name_index(ctx, xdoc);
|
|
417
|
+
mkr_xml_register_query_namespaces(ctx, ns); /* frees ctx + raises on error */
|
|
418
|
+
|
|
419
|
+
mkr_node_t *ast = mkr_css_compile_or_raise(ctx, selector, ns); /* frees ctx + raises on error */
|
|
420
|
+
|
|
421
|
+
mkr_xpath_value_t value = {0};
|
|
422
|
+
mkr_xpath_error_t error = {0};
|
|
423
|
+
int rc = mkr_xpath_eval_compiled(ctx, ast, &value, &error);
|
|
424
|
+
mkr_node_free(ast);
|
|
425
|
+
if (rc != 0) {
|
|
426
|
+
mkr_xpath_context_free(ctx);
|
|
427
|
+
mkr_xpath_raise(&error);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
int found = 0;
|
|
431
|
+
if (value.type == MKR_XPATH_TYPE_NODESET) {
|
|
432
|
+
for (size_t i = 0; i < value.u.nodeset.count; i++) {
|
|
433
|
+
if (value.u.nodeset.nodes[i] == (void *)node) { found = 1; break; }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
mkr_xpath_value_clear(&value);
|
|
437
|
+
mkr_xpath_context_free(ctx);
|
|
438
|
+
return found ? Qtrue : Qfalse;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* The document's root element. */
|
|
442
|
+
static VALUE
|
|
443
|
+
mkr_xml_doc_root(VALUE self)
|
|
444
|
+
{
|
|
445
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(self));
|
|
446
|
+
return (xdoc == NULL) ? Qnil : mkr_wrap_xml_node(xdoc->root, self);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* The document's DOCTYPE as a Makiri::XML::DocumentType (aliased
|
|
450
|
+
* Makiri::XML::DTD), or nil if the document had no
|
|
451
|
+
* `<!DOCTYPE ...>`. Mirrors Nokogiri's Document#internal_subset. The DTD's name
|
|
452
|
+
* and external/system identifiers are read; the DTD body is NOT parsed (no
|
|
453
|
+
* entity/element declarations are loaded - &name; stays an undefined-entity
|
|
454
|
+
* error and no external subset is fetched). The doctype node is kept off the
|
|
455
|
+
* tree, so XPath never sees it (XPath 1.0 has no doctype node type). */
|
|
456
|
+
static VALUE
|
|
457
|
+
mkr_xml_doc_internal_subset(VALUE self)
|
|
458
|
+
{
|
|
459
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(self));
|
|
460
|
+
return (xdoc == NULL || xdoc->doctype == NULL)
|
|
461
|
+
? Qnil
|
|
462
|
+
: mkr_wrap_xml_node(xdoc->doctype, self);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* Map a fragment-parse status to its Ruby exception (never returns on error). */
|
|
466
|
+
NORETURN(static void mkr_xml_raise_fragment_status(mkr_xml_status_t st));
|
|
467
|
+
static void
|
|
468
|
+
mkr_xml_raise_fragment_status(mkr_xml_status_t st)
|
|
469
|
+
{
|
|
470
|
+
switch (st) {
|
|
471
|
+
case MKR_XML_ERR_SYNTAX: rb_raise(mkr_eXmlSyntaxError, "malformed XML fragment");
|
|
472
|
+
case MKR_XML_ERR_LIMIT: rb_raise(mkr_eXmlLimitExceeded, "XML fragment budget exceeded");
|
|
473
|
+
case MKR_XML_ERR_VERSION: rb_raise(mkr_eXmlSyntaxError,
|
|
474
|
+
"unsupported XML version (only XML 1.0 is supported)");
|
|
475
|
+
default: rb_raise(mkr_eError, "failed to parse XML fragment");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* Strict-decode +rb_source+ and parse it as a fragment into +xdoc+ (when
|
|
480
|
+
* +inherit_doc_ns+, names resolve against the document's root namespaces). The
|
|
481
|
+
* parse runs under the GVL: a fragment is small, and an existing document's arena
|
|
482
|
+
* must never be mutated with the GVL released. Returns the DOCUMENT_FRAGMENT node;
|
|
483
|
+
* raises on a decode/parse failure. */
|
|
484
|
+
static mkr_xml_node_t *
|
|
485
|
+
mkr_xml_fragment_into(mkr_xml_doc_t *xdoc, VALUE rb_source, int inherit_doc_ns)
|
|
486
|
+
{
|
|
487
|
+
VALUE decoded = mkr_xml_decode_input(rb_String(rb_source), xdoc->max_bytes);
|
|
488
|
+
mkr_owned_bytes_t src = { 0 };
|
|
489
|
+
if (mkr_ruby_copy_bytes(decoded, &src) != 0) {
|
|
490
|
+
rb_raise(mkr_eError, "out of memory copying XML fragment source");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
mkr_xml_status_t st = MKR_XML_OK;
|
|
494
|
+
mkr_xml_node_t *frag = mkr_xml_parse_fragment(xdoc, src.ptr, src.len, inherit_doc_ns, &st);
|
|
495
|
+
mkr_owned_bytes_clear(&src);
|
|
496
|
+
if (frag == NULL) {
|
|
497
|
+
mkr_xml_raise_fragment_status(st);
|
|
498
|
+
}
|
|
499
|
+
return frag;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* A fresh, empty XML Document VALUE: a backing arena holding a DOCUMENT node and
|
|
503
|
+
* no root element. Used by Document.new and as DocumentFragment.parse's backing
|
|
504
|
+
* document. Raises on OOM (with +parsed+ already GC-owned, so it frees cleanly). */
|
|
505
|
+
static VALUE
|
|
506
|
+
mkr_xml_new_empty_document(void)
|
|
507
|
+
{
|
|
508
|
+
mkr_parsed_t *parsed = mkr_parsed_new_xml(NULL);
|
|
509
|
+
if (parsed == NULL) {
|
|
510
|
+
rb_raise(mkr_eError, "out of memory allocating XML document");
|
|
511
|
+
}
|
|
512
|
+
VALUE doc_obj = mkr_wrap_document(parsed); /* GC owns +parsed+ from here */
|
|
513
|
+
mkr_xml_doc_t *xdoc = mkr_xml_doc_new();
|
|
514
|
+
if (xdoc == NULL) {
|
|
515
|
+
rb_raise(mkr_eError, "out of memory allocating XML document");
|
|
516
|
+
}
|
|
517
|
+
mkr_parsed_set_xml_doc(parsed, xdoc); /* GC now frees +xdoc+ via +parsed+ */
|
|
518
|
+
xdoc->doc_node = mkr_xml_arena_node(xdoc, MKR_XML_NODE_TYPE_DOCUMENT);
|
|
519
|
+
if (xdoc->doc_node == NULL) {
|
|
520
|
+
rb_raise(mkr_eError, "out of memory allocating XML document");
|
|
521
|
+
}
|
|
522
|
+
return doc_obj;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* call-seq: Makiri::XML::Document.new -> Document
|
|
526
|
+
* A new, empty XML document (no root element) to build up programmatically with
|
|
527
|
+
* #create_element etc. and #add_child / #root=, like Nokogiri. Any arguments
|
|
528
|
+
* (Nokogiri accepts a version / encoding) are accepted and ignored. */
|
|
529
|
+
static VALUE
|
|
530
|
+
mkr_xml_document_s_new(int argc, VALUE *argv, VALUE klass)
|
|
531
|
+
{
|
|
532
|
+
(void)argc; (void)argv; (void)klass;
|
|
533
|
+
return mkr_xml_new_empty_document();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* call-seq: Makiri::XML::DocumentFragment.parse(source) -> DocumentFragment
|
|
537
|
+
* Parse +source+ into a standalone fragment with its own (empty) backing
|
|
538
|
+
* document. The fragment is self-contained: a prefixed name must declare its
|
|
539
|
+
* namespace within the fragment itself (use Document#fragment to parse against an
|
|
540
|
+
* existing document's in-scope namespaces). */
|
|
541
|
+
static VALUE
|
|
542
|
+
mkr_xml_fragment_s_parse(VALUE klass, VALUE rb_source)
|
|
543
|
+
{
|
|
544
|
+
(void)klass;
|
|
545
|
+
VALUE doc_obj = mkr_xml_new_empty_document();
|
|
546
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(doc_obj));
|
|
547
|
+
mkr_xml_node_t *frag = mkr_xml_fragment_into(xdoc, rb_source, 0);
|
|
548
|
+
VALUE result = mkr_wrap_xml_node(frag, doc_obj);
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* call-seq: doc.fragment(source) -> DocumentFragment
|
|
553
|
+
* Parse +source+ into a fragment bound to this document, resolving names against
|
|
554
|
+
* the document's in-scope (root) namespaces, so the fragment's nodes can be
|
|
555
|
+
* spliced in with Node#add_child and friends. */
|
|
556
|
+
static VALUE
|
|
557
|
+
mkr_xml_doc_fragment(VALUE self, VALUE rb_source)
|
|
558
|
+
{
|
|
559
|
+
mkr_xml_doc_t *xdoc = mkr_parsed_xml_doc(mkr_doc_parsed(self));
|
|
560
|
+
if (xdoc == NULL) {
|
|
561
|
+
rb_raise(mkr_eError, "the document has no arena");
|
|
562
|
+
}
|
|
563
|
+
mkr_xml_node_t *frag = mkr_xml_fragment_into(xdoc, rb_source, 1);
|
|
564
|
+
VALUE result = mkr_wrap_xml_node(frag, self);
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
void
|
|
569
|
+
mkr_init_xml(void)
|
|
570
|
+
{
|
|
571
|
+
/* XML::Document is a Makiri::Document leaf (§12): is_a?(Makiri::Document) is
|
|
572
|
+
* true, but it carries no HTML readers (those are on Makiri::HTML, which it
|
|
573
|
+
* does not include) - the read-only XML surface is structural. */
|
|
574
|
+
mkr_cXmlDocument = rb_define_class_under(mkr_mXML, "Document", mkr_cDocument);
|
|
575
|
+
rb_undef_alloc_func(mkr_cXmlDocument); /* created only from C, never .new */
|
|
576
|
+
rb_include_module(mkr_cXmlDocument, mkr_mXmlNodeMethods);
|
|
577
|
+
|
|
578
|
+
rb_define_method(mkr_cXmlDocument, "root", mkr_xml_doc_root, 0);
|
|
579
|
+
rb_define_method(mkr_cXmlDocument, "internal_subset", mkr_xml_doc_internal_subset, 0);
|
|
580
|
+
rb_define_method(mkr_cXmlDocument, "fragment", mkr_xml_doc_fragment, 1);
|
|
581
|
+
rb_define_singleton_method(mkr_cXmlDocument, "new", mkr_xml_document_s_new, -1);
|
|
582
|
+
rb_define_singleton_method(mkr_cXmlDocumentFragment, "parse", mkr_xml_fragment_s_parse, 1);
|
|
583
|
+
|
|
584
|
+
/* xpath / at_xpath work on the document and on any XML node (rooted at that
|
|
585
|
+
* node), so they live on the shared XML node behavior module + the document. */
|
|
586
|
+
rb_define_method(mkr_cXmlDocument, "xpath", mkr_xml_doc_xpath, -1);
|
|
587
|
+
rb_define_method(mkr_cXmlDocument, "at_xpath", mkr_xml_doc_at_xpath, -1);
|
|
588
|
+
rb_define_method(mkr_mXmlNodeMethods, "xpath", mkr_xml_doc_xpath, -1);
|
|
589
|
+
rb_define_method(mkr_mXmlNodeMethods, "at_xpath", mkr_xml_doc_at_xpath, -1);
|
|
590
|
+
|
|
591
|
+
/* CSS selectors over XML: private primitives called by the Ruby #css /
|
|
592
|
+
* #at_css / #matches? wrappers (which collect the document namespaces). The
|
|
593
|
+
* selector is lowered to the native XPath engine (mkr_css.c). */
|
|
594
|
+
rb_define_private_method(mkr_mXmlNodeMethods, "_css", mkr_xml_node_css, 2);
|
|
595
|
+
rb_define_private_method(mkr_mXmlNodeMethods, "_at_css", mkr_xml_node_at_css, 2);
|
|
596
|
+
rb_define_private_method(mkr_mXmlNodeMethods, "_css_matches", mkr_xml_node_css_matches, 2);
|
|
597
|
+
|
|
598
|
+
/* The native XML parser, exposed as XML::Document.parse, mirroring HTML
|
|
599
|
+
* (HTML::Document.parse). The Makiri::XML(source) convenience delegates to it
|
|
600
|
+
* in Ruby (lib/makiri.rb), as Makiri.HTML does for HTML::Document.parse. */
|
|
601
|
+
rb_define_singleton_method(mkr_cXmlDocument, "parse", mkr_xml_s_parse, -1);
|
|
602
|
+
}
|