leptris 1.9.199.0-arm-linux
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 +7 -0
- data/CHANGELOG.md +3890 -0
- data/CLAUDE.md +115 -0
- data/CONTEXT.md +39 -0
- data/LICENSE.md +33 -0
- data/README.adoc +755 -0
- data/Rakefile +407 -0
- data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
- data/TODO.perf/02-auto-enable-native-layer.md +19 -0
- data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
- data/TODO.perf/04-ext-bound-serialization.md +15 -0
- data/TODO.perf/05-native-surface-completion.md +19 -0
- data/TODO.perf/06-perf-battery-native-legs.md +16 -0
- data/TODO.perf/07-native-read-floor.md +31 -0
- data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
- data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
- data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
- data/TODO.perf/11-moxml-gap-battery.md +18 -0
- data/TODO.perf/12-document-lifetime-in-c.md +47 -0
- data/TODO.perf/13-structural-memos.md +49 -0
- data/TODO.perf/14-insert-family-c-bound.md +31 -0
- data/TODO.perf/15-compiled-expression-cache.md +35 -0
- data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
- data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
- data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
- data/TODO.perf/19-lazy-node-pointer.md +39 -0
- data/TODO.perf/20-css-translation-cache.md +24 -0
- data/TODO.perf/21-key-memo-consult.md +20 -0
- data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
- data/TODO.perf/23-cbound-value-mutations.md +27 -0
- data/TODO.perf/24-immutable-read-lanes.md +22 -0
- data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
- data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
- data/TODO.perf/27-c-yield-traversal.md +38 -0
- data/TODO.perf/28-address-fills.md +25 -0
- data/TODO.perf/29-ns-xpath-compiled.md +23 -0
- data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
- data/TODO.perf/31-battery-rounds-4-8.md +23 -0
- data/TODO.perf/32-clean-host-floor-table.md +29 -0
- data/TODO.perf/33-cbound-root-set.md +29 -0
- data/TODO.perf/34-fragment-fast-lane.md +41 -0
- data/TODO.perf/35-parse-default-c-face.md +24 -0
- data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
- data/TODO.perf/37-document-lazy-pointer.md +28 -0
- data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
- data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
- data/TODO.restructure/02-deep-copy-seam.md +21 -0
- data/TODO.restructure/03-evaluation-context-seam.md +18 -0
- data/TODO.restructure/04-spec-mece-restructure.md +20 -0
- data/TODO.restructure/05-memory-documentation.md +13 -0
- data/TODO.restructure/06-architecture-map-refresh.md +12 -0
- data/TODO.restructure/07-external-gates.md +16 -0
- data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
- data/TODO.restructure/09-iteration-scope.md +29 -0
- data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
- data/TODO.restructure/11-construction-factory-diet.md +16 -0
- data/TODO.restructure/12-resultattr-merge.md +12 -0
- data/TODO.restructure/13-spec-mece-followup.md +9 -0
- data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
- data/TODO.restructure/15-readme-feature-documentation.md +10 -0
- data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
- data/TODO.restructure/17-perf-drift-record.md +12 -0
- data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
- data/TODO.restructure/19-gitignore-hygiene.md +10 -0
- data/TODO.restructure/20-utf8proc-enablement.md +34 -0
- data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
- data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
- data/docs/adr/0001-lockstep-mirror.md +17 -0
- data/docs/adr/0002-utf8-at-the-seam.md +19 -0
- data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
- data/docs/adr/0004-lifetime-guard.md +21 -0
- data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
- data/docs/adr/0006-ruby-variant-policy.md +25 -0
- data/ext/build_windows_native.rb +42 -0
- data/ext/leptris/native/extconf.rb +55 -0
- data/ext/leptris/native/native.c +2528 -0
- data/leptris.gemspec +43 -0
- data/lib/leptris/version.rb +5 -0
- data/lib/leptris/xml/attr.rb +76 -0
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +27 -0
- data/lib/leptris/xml/comment.rb +27 -0
- data/lib/leptris/xml/css_to_xpath.rb +192 -0
- data/lib/leptris/xml/descriptor.rb +214 -0
- data/lib/leptris/xml/diff.rb +74 -0
- data/lib/leptris/xml/doc_type.rb +54 -0
- data/lib/leptris/xml/document.rb +716 -0
- data/lib/leptris/xml/document_fragment.rb +63 -0
- data/lib/leptris/xml/element.rb +771 -0
- data/lib/leptris/xml/entity_reference.rb +13 -0
- data/lib/leptris/xml/evaluation_context.rb +32 -0
- data/lib/leptris/xml/ffi.rb +1527 -0
- data/lib/leptris/xml/iteration_scope.rb +56 -0
- data/lib/leptris/xml/iterparse.rb +144 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/native.so +0 -0
- data/lib/leptris/xml/native_layer.rb +92 -0
- data/lib/leptris/xml/node.rb +686 -0
- data/lib/leptris/xml/node_set.rb +212 -0
- data/lib/leptris/xml/parse_options.rb +97 -0
- data/lib/leptris/xml/plan_value.rb +121 -0
- data/lib/leptris/xml/processing_instruction.rb +90 -0
- data/lib/leptris/xml/pull.rb +212 -0
- data/lib/leptris/xml/relaxng.rb +126 -0
- data/lib/leptris/xml/result_attr.rb +39 -0
- data/lib/leptris/xml/result_text.rb +36 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
- data/lib/leptris/xml/sax/parser.rb +326 -0
- data/lib/leptris/xml/sax/recorder.rb +374 -0
- data/lib/leptris/xml/sax.rb +14 -0
- data/lib/leptris/xml/schematron.rb +88 -0
- data/lib/leptris/xml/searchable.rb +361 -0
- data/lib/leptris/xml/serialization.rb +180 -0
- data/lib/leptris/xml/text.rb +32 -0
- data/lib/leptris/xml/xpath.rb +89 -0
- data/lib/leptris/xml/xquery.rb +77 -0
- data/lib/leptris/xml/xslt.rb +85 -0
- data/lib/leptris/xml.rb +108 -0
- data/lib/leptris.rb +53 -0
- data/lib/libleptris.so +0 -0
- data/lib/libutf8proc.so.3 +0 -0
- data/scripts/gem_smoke.rb +36 -0
- metadata +214 -0
|
@@ -0,0 +1,2528 @@
|
|
|
1
|
+
/* Native TypedData node prototype (#185/#147/#187 converged):
|
|
2
|
+
* wrapper objects with the C pointer embedded (no FFI::Pointer per
|
|
3
|
+
* node), hot reads (name/content/attribute) called directly in C
|
|
4
|
+
* via dlsym on the FFI-loaded library, and children constructed in
|
|
5
|
+
* bulk — one cache round-trip, zero Ruby frames, zero FFI
|
|
6
|
+
* marshaling per node. */
|
|
7
|
+
#include <ruby.h>
|
|
8
|
+
#include <ruby/encoding.h>
|
|
9
|
+
#include <stdint.h>
|
|
10
|
+
#include <string.h>
|
|
11
|
+
|
|
12
|
+
#ifdef _WIN32
|
|
13
|
+
#include <windows.h>
|
|
14
|
+
#else
|
|
15
|
+
#include <dlfcn.h>
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
typedef const char *(*elem_name_fn)(void *);
|
|
19
|
+
typedef const char *(*text_content_fn)(void *);
|
|
20
|
+
typedef const char *(*attr_fn)(void *, const char *);
|
|
21
|
+
typedef size_t (*children_ex_fn)(void *, void **, int *, size_t);
|
|
22
|
+
typedef int (*node_type_fn)(void *);
|
|
23
|
+
typedef void *(*next_sibling_fn)(void *);
|
|
24
|
+
typedef void *(*parent_fn)(void *);
|
|
25
|
+
typedef const char *(*elem_prefix_fn)(void *);
|
|
26
|
+
typedef const char *(*elem_ns_fn)(void *);
|
|
27
|
+
typedef size_t (*xp_count_fn)(void *);
|
|
28
|
+
typedef size_t (*xp_nodes_ex_fn)(void *, void **, int *, size_t);
|
|
29
|
+
typedef int (*xp_node_kind_fn)(void *, int);
|
|
30
|
+
typedef const char *(*xp_node_name_fn)(void *, int);
|
|
31
|
+
typedef const char *(*xp_node_value_fn)(void *, int);
|
|
32
|
+
typedef size_t (*serialize_into_fn)(void *, char *, size_t, void *, void *);
|
|
33
|
+
typedef int (*ns_count_fn)(void *);
|
|
34
|
+
typedef void *(*attr_first_fn)(void *);
|
|
35
|
+
typedef void *(*attr_next_fn)(void *);
|
|
36
|
+
typedef const char *(*attr_name_fn)(void *);
|
|
37
|
+
typedef const char *(*attr_value_fn)(void *, void *);
|
|
38
|
+
typedef unsigned int (*node_line_fn)(void *);
|
|
39
|
+
typedef size_t (*node_offset_fn)(void *);
|
|
40
|
+
typedef const char *(*element_text_fn)(void *);
|
|
41
|
+
typedef void *(*doc_create_fn)(void);
|
|
42
|
+
typedef void *(*elem_create_fn)(void *, const char *);
|
|
43
|
+
typedef void *(*text_create_fn)(void *, const char *);
|
|
44
|
+
typedef void *(*create_child_fn)(void *, const char *);
|
|
45
|
+
typedef int (*append_child_fn)(void *, void *);
|
|
46
|
+
typedef int (*set_attr_fn)(void *, const char *, const char *);
|
|
47
|
+
typedef int (*set_root_fn)(void *, void *);
|
|
48
|
+
typedef void (*doc_free_fn)(void *);
|
|
49
|
+
|
|
50
|
+
static elem_name_fn f_elem_name;
|
|
51
|
+
static text_content_fn f_text_content;
|
|
52
|
+
static attr_fn f_attr;
|
|
53
|
+
static children_ex_fn f_children_ex;
|
|
54
|
+
static node_type_fn f_node_type;
|
|
55
|
+
static next_sibling_fn f_next_sibling;
|
|
56
|
+
static parent_fn f_parent;
|
|
57
|
+
static elem_prefix_fn f_elem_prefix;
|
|
58
|
+
static elem_ns_fn f_elem_ns;
|
|
59
|
+
static xp_count_fn f_xp_count;
|
|
60
|
+
static xp_nodes_ex_fn f_xp_nodes_ex;
|
|
61
|
+
static xp_node_kind_fn f_xp_node_kind;
|
|
62
|
+
static xp_node_name_fn f_xp_node_name;
|
|
63
|
+
static xp_node_value_fn f_xp_node_value;
|
|
64
|
+
static serialize_into_fn f_doc_serialize, f_elem_serialize;
|
|
65
|
+
static ns_count_fn f_elem_ns_count;
|
|
66
|
+
static attr_first_fn f_attr_first;
|
|
67
|
+
static attr_next_fn f_attr_next;
|
|
68
|
+
static attr_name_fn f_attr_name;
|
|
69
|
+
static attr_value_fn f_attr_value;
|
|
70
|
+
static node_line_fn f_node_line;
|
|
71
|
+
static node_offset_fn f_node_offset;
|
|
72
|
+
static element_text_fn f_element_text;
|
|
73
|
+
static doc_create_fn f_doc_create;
|
|
74
|
+
static elem_create_fn f_elem_create;
|
|
75
|
+
static text_create_fn f_text_create;
|
|
76
|
+
static create_child_fn f_create_child;
|
|
77
|
+
static append_child_fn f_append_child;
|
|
78
|
+
static append_child_fn f_prepend_child, f_insert_after, f_insert_before;
|
|
79
|
+
static set_attr_fn f_set_attr;
|
|
80
|
+
typedef int (*xp_type_fn)(void *);
|
|
81
|
+
typedef void (*xp_free_fn)(void *);
|
|
82
|
+
typedef void *(*first_child_fn)(void *);
|
|
83
|
+
typedef const char *(*node_str_fn)(void *);
|
|
84
|
+
static xp_type_fn f_xp_type;
|
|
85
|
+
static xp_free_fn f_xp_free;
|
|
86
|
+
static first_child_fn f_first_child;
|
|
87
|
+
static node_str_fn f_comment_content, f_cdata_content, f_pi_target,
|
|
88
|
+
f_pi_data;
|
|
89
|
+
typedef int (*set_str_fn)(void *, const char *);
|
|
90
|
+
typedef int (*node_unlink_fn)(void *);
|
|
91
|
+
typedef int (*traverse_cb_fn)(void *, void *);
|
|
92
|
+
typedef int (*traverse_fn)(void *, int, traverse_cb_fn, void *);
|
|
93
|
+
typedef void (*visit_cb_fn)(void *, void *, int, int);
|
|
94
|
+
typedef void (*visit_fn)(void *, visit_cb_fn, void *);
|
|
95
|
+
typedef void (*free_str_fn)(void *);
|
|
96
|
+
typedef const char *(*node_xpath_fn)(void *);
|
|
97
|
+
typedef void *(*elem_copy_fn)(void *, void *);
|
|
98
|
+
typedef void *(*parse_str_fn)(const char *, size_t, void *);
|
|
99
|
+
typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
|
|
100
|
+
typedef const char *(*last_err_fn)(void);
|
|
101
|
+
typedef const char *(*status_str_fn)(int);
|
|
102
|
+
static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
|
|
103
|
+
static node_unlink_fn f_node_unlink;
|
|
104
|
+
static traverse_fn f_node_traverse;
|
|
105
|
+
static visit_fn f_node_visit;
|
|
106
|
+
static free_str_fn f_free_str;
|
|
107
|
+
static node_xpath_fn f_node_xpath;
|
|
108
|
+
static elem_copy_fn f_elem_copy;
|
|
109
|
+
static parse_str_fn f_parse_str;
|
|
110
|
+
static parse_frag_fn f_parse_frag;
|
|
111
|
+
static last_err_fn f_last_err;
|
|
112
|
+
static status_str_fn f_status_str;
|
|
113
|
+
static set_root_fn f_set_root;
|
|
114
|
+
static doc_free_fn f_doc_free;
|
|
115
|
+
|
|
116
|
+
static VALUE c_native_node;
|
|
117
|
+
/* Ivar reads (TODO.perf/07 tail): the binding Document keeps the
|
|
118
|
+
* C address and the caches in plain ivars, so the hot C faces skip
|
|
119
|
+
* rb_funcall dispatch entirely. */
|
|
120
|
+
static ID id_iv_c_address, id_iv_native_cache, id_iv_binding_cache;
|
|
121
|
+
static ID id_iv_version, id_iv_readonly, id_address;
|
|
122
|
+
static ID id_iv_nn_content, id_iv_nn_content_ver;
|
|
123
|
+
static ID id_iv_nn_attrs, id_iv_nn_attrs_ver;
|
|
124
|
+
static ID id_child_klasses;
|
|
125
|
+
static VALUE c_use_after_free_error;
|
|
126
|
+
|
|
127
|
+
static VALUE native_cache_of(VALUE document);
|
|
128
|
+
static void resolve_binding_classes(void);
|
|
129
|
+
static VALUE binding_cache_of(VALUE document);
|
|
130
|
+
static VALUE binding_klass_for(int kind);
|
|
131
|
+
static VALUE c_iteration_scope;
|
|
132
|
+
static VALUE c_leptris_error;
|
|
133
|
+
static VALUE c_b_document, c_b_freed, c_ffi_pointer;
|
|
134
|
+
static ID id_ptr_new, id_freed_new, id_alive;
|
|
135
|
+
static void check_status_c(int st);
|
|
136
|
+
|
|
137
|
+
#define NT_ELEMENT 0
|
|
138
|
+
|
|
139
|
+
struct native_node {
|
|
140
|
+
void *ptr;
|
|
141
|
+
VALUE document;
|
|
142
|
+
/* Single-slot read memos (#204 ask 1): the last attribute
|
|
143
|
+
* query and the last content, stamped with the document's
|
|
144
|
+
* mutation version (a Fixnum immediate — pointer compare).
|
|
145
|
+
* Slot beats the hash memo on repeat reads of one name. */
|
|
146
|
+
VALUE attr_key, attr_val, attr_ver;
|
|
147
|
+
VALUE content_val, content_ver;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
static void nn_mark(void *p)
|
|
151
|
+
{
|
|
152
|
+
struct native_node *n = p;
|
|
153
|
+
if (n->document != Qnil)
|
|
154
|
+
rb_gc_mark(n->document);
|
|
155
|
+
if (n->attr_key != Qnil)
|
|
156
|
+
rb_gc_mark(n->attr_key);
|
|
157
|
+
if (n->attr_val != Qnil)
|
|
158
|
+
rb_gc_mark(n->attr_val);
|
|
159
|
+
if (n->content_val != Qnil)
|
|
160
|
+
rb_gc_mark(n->content_val);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static size_t nn_size(const void *p) { (void)p; return sizeof(struct native_node); }
|
|
164
|
+
|
|
165
|
+
static const rb_data_type_t nn_type = {
|
|
166
|
+
"Leptris/XML/NativeNode",
|
|
167
|
+
{ nn_mark, RUBY_TYPED_DEFAULT_FREE, nn_size, },
|
|
168
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
#ifdef _WIN32
|
|
172
|
+
static void *lib_open(const char *path) { return (void *)LoadLibraryA(path); }
|
|
173
|
+
static void *lib_sym(void *h, const char *name)
|
|
174
|
+
{
|
|
175
|
+
return (void *)GetProcAddress((HMODULE)h, name);
|
|
176
|
+
}
|
|
177
|
+
#else
|
|
178
|
+
static void *lib_open(const char *path) { return dlopen(path, RTLD_NOW); }
|
|
179
|
+
static void *lib_sym(void *h, const char *name) { return dlsym(h, name); }
|
|
180
|
+
#endif
|
|
181
|
+
|
|
182
|
+
static void resolve_symbols(const char *lib_path)
|
|
183
|
+
{
|
|
184
|
+
void *h = lib_open(lib_path);
|
|
185
|
+
if (!h)
|
|
186
|
+
rb_raise(rb_eRuntimeError, "cannot load library %s", lib_path);
|
|
187
|
+
f_elem_name = (elem_name_fn)lib_sym(h, "leptris_element_name");
|
|
188
|
+
f_text_content = (text_content_fn)lib_sym(h, "leptris_text_node_get_content");
|
|
189
|
+
f_attr = (attr_fn)lib_sym(h, "leptris_element_attribute");
|
|
190
|
+
f_children_ex = (children_ex_fn)lib_sym(h, "leptris_node_children_ex");
|
|
191
|
+
f_node_type = (node_type_fn)lib_sym(h, "leptris_node_get_type");
|
|
192
|
+
f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
|
|
193
|
+
f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
|
|
194
|
+
f_elem_prefix = (elem_prefix_fn)lib_sym(h, "leptris_element_prefix");
|
|
195
|
+
f_elem_ns_count = (ns_count_fn)lib_sym(h, "leptris_element_namespace_count");
|
|
196
|
+
f_elem_ns = (elem_ns_fn)lib_sym(h, "leptris_element_namespace");
|
|
197
|
+
f_xp_count = (xp_count_fn)lib_sym(h, "leptris_xpath_result_count");
|
|
198
|
+
f_xp_nodes_ex = (xp_nodes_ex_fn)lib_sym(h, "leptris_xpath_result_get_nodes_ex");
|
|
199
|
+
f_xp_node_kind = (xp_node_kind_fn)lib_sym(h, "leptris_xpath_result_node_kind");
|
|
200
|
+
f_xp_node_name = (xp_node_name_fn)lib_sym(h, "leptris_xpath_result_node_name");
|
|
201
|
+
f_xp_node_value = (xp_node_value_fn)lib_sym(h, "leptris_xpath_result_node_value");
|
|
202
|
+
f_doc_serialize = (serialize_into_fn)lib_sym(h, "leptris_document_serialize_into");
|
|
203
|
+
f_elem_serialize = (serialize_into_fn)lib_sym(h, "leptris_element_serialize_into");
|
|
204
|
+
f_attr_first = (attr_first_fn)lib_sym(h, "leptris_element_first_attribute");
|
|
205
|
+
f_attr_next = (attr_next_fn)lib_sym(h, "leptris_attribute_next");
|
|
206
|
+
f_attr_name = (attr_name_fn)lib_sym(h, "leptris_attribute_get_name");
|
|
207
|
+
f_attr_value = (attr_value_fn)lib_sym(h, "leptris_attribute_get_value");
|
|
208
|
+
f_node_line = (node_line_fn)lib_sym(h, "leptris_node_line");
|
|
209
|
+
f_node_offset = (node_offset_fn)lib_sym(h, "leptris_node_byte_offset");
|
|
210
|
+
f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
|
|
211
|
+
f_doc_create = (doc_create_fn)lib_sym(h, "leptris_document_create");
|
|
212
|
+
f_elem_create = (elem_create_fn)lib_sym(h, "leptris_element_create");
|
|
213
|
+
f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
|
|
214
|
+
f_create_child = (create_child_fn)lib_sym(h, "leptris_element_create_child");
|
|
215
|
+
f_append_child = (append_child_fn)lib_sym(h, "leptris_element_append_child");
|
|
216
|
+
f_prepend_child = (append_child_fn)lib_sym(h, "leptris_element_prepend_child");
|
|
217
|
+
f_insert_after = (append_child_fn)lib_sym(h, "leptris_element_insert_after");
|
|
218
|
+
f_insert_before = (append_child_fn)lib_sym(h, "leptris_element_insert_before");
|
|
219
|
+
f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
|
|
220
|
+
f_xp_type = (xp_type_fn)lib_sym(h, "leptris_xpath_result_type");
|
|
221
|
+
f_xp_free = (xp_free_fn)lib_sym(h, "leptris_xpath_result_free");
|
|
222
|
+
f_first_child = (first_child_fn)lib_sym(h, "leptris_node_first_child");
|
|
223
|
+
f_comment_content = (node_str_fn)lib_sym(h, "leptris_comment_node_get_content");
|
|
224
|
+
f_cdata_content = (node_str_fn)lib_sym(h, "leptris_cdata_node_get_content");
|
|
225
|
+
f_pi_target = (node_str_fn)lib_sym(h, "leptris_pi_node_get_target");
|
|
226
|
+
f_pi_data = (node_str_fn)lib_sym(h, "leptris_pi_node_get_data");
|
|
227
|
+
f_elem_set_name = (set_str_fn)lib_sym(h, "leptris_element_set_name");
|
|
228
|
+
f_elem_set_text = (set_str_fn)lib_sym(h, "leptris_element_set_text");
|
|
229
|
+
f_text_set_content = (set_str_fn)lib_sym(h, "leptris_text_node_set_content");
|
|
230
|
+
f_node_unlink = (node_unlink_fn)lib_sym(h, "leptris_node_unlink");
|
|
231
|
+
f_node_traverse = (traverse_fn)lib_sym(h, "leptris_node_traverse");
|
|
232
|
+
f_node_visit = (visit_fn)lib_sym(h, "leptris_node_visit");
|
|
233
|
+
f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
|
|
234
|
+
f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
|
|
235
|
+
f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
|
|
236
|
+
f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
|
|
237
|
+
f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
|
|
238
|
+
f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
|
|
239
|
+
f_status_str = (status_str_fn)lib_sym(h, "leptris_status_string");
|
|
240
|
+
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
241
|
+
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
242
|
+
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
243
|
+
!f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
|
|
244
|
+
!f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
|
|
245
|
+
!f_create_child || !f_append_child || !f_prepend_child ||
|
|
246
|
+
!f_insert_after || !f_insert_before || !f_set_attr || !f_set_root ||
|
|
247
|
+
!f_xp_type || !f_xp_free || !f_first_child ||
|
|
248
|
+
!f_comment_content || !f_cdata_content || !f_pi_target ||
|
|
249
|
+
!f_pi_data || !f_elem_set_name || !f_elem_set_text ||
|
|
250
|
+
!f_text_set_content || !f_node_unlink ||
|
|
251
|
+
!f_node_traverse || !f_node_visit || !f_free_str ||
|
|
252
|
+
!f_node_xpath || !f_elem_copy || !f_parse_str ||
|
|
253
|
+
!f_parse_frag || !f_last_err || !f_status_str ||
|
|
254
|
+
!f_doc_free ||
|
|
255
|
+
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
256
|
+
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
257
|
+
!f_doc_serialize || !f_elem_serialize || !f_attr_first ||
|
|
258
|
+
!f_elem_ns_count || !f_elem_ns ||
|
|
259
|
+
!f_attr_next || !f_attr_name || !f_attr_value ||
|
|
260
|
+
!f_node_line || !f_node_offset)
|
|
261
|
+
rb_raise(rb_eRuntimeError, "libleptris symbols missing");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static VALUE nn_name(VALUE self)
|
|
265
|
+
{
|
|
266
|
+
struct native_node *n;
|
|
267
|
+
const char *s;
|
|
268
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
269
|
+
s = f_elem_name(n->ptr);
|
|
270
|
+
/* Interned (shared, frozen) fstring: element names repeat
|
|
271
|
+
* massively across a document — one RString per distinct name
|
|
272
|
+
* per process instead of one per read (lutaml/moxml#249:
|
|
273
|
+
* ~18% of consumer materialize allocations were fresh name
|
|
274
|
+
* strings). Names are identifiers; nothing downstream mutates
|
|
275
|
+
* them in place (renames mint new strings). */
|
|
276
|
+
return s ? rb_enc_interned_str(s, strlen(s), rb_utf8_encoding()) : Qnil;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static VALUE nn_content(VALUE self)
|
|
280
|
+
{
|
|
281
|
+
struct native_node *n;
|
|
282
|
+
const char *s;
|
|
283
|
+
VALUE doc, cached;
|
|
284
|
+
|
|
285
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
286
|
+
/* Version-stamped content memo (the binding Document's
|
|
287
|
+
* mutation version — the same invalidation discipline as
|
|
288
|
+
* binding memos, ADR 0003): repeat reads are two struct
|
|
289
|
+
* reads and a pointer compare. nil results never cache
|
|
290
|
+
* (empty elements recompute — rare in hot walks). */
|
|
291
|
+
doc = n->document;
|
|
292
|
+
if (doc != Qnil && n->content_ver != Qnil &&
|
|
293
|
+
n->content_ver == rb_ivar_get(doc, id_iv_version))
|
|
294
|
+
return n->content_val;
|
|
295
|
+
/* elements aggregate their text (leptris_element_text); text
|
|
296
|
+
* nodes carry it directly. */
|
|
297
|
+
s = f_node_type(n->ptr) == 0 ? f_element_text(n->ptr)
|
|
298
|
+
: f_text_content(n->ptr);
|
|
299
|
+
cached = s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
300
|
+
if (doc != Qnil && cached != Qnil) {
|
|
301
|
+
n->content_val = cached;
|
|
302
|
+
n->content_ver = rb_ivar_get(doc, id_iv_version);
|
|
303
|
+
}
|
|
304
|
+
return cached;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
static VALUE nf_fast_attribute2(VALUE self, VALUE addr, VALUE name)
|
|
308
|
+
{
|
|
309
|
+
const char *s;
|
|
310
|
+
(void)self;
|
|
311
|
+
s = f_attr((void *)(uintptr_t)NUM2ULL(addr),
|
|
312
|
+
RSTRING_PTR(StringValue(name)));
|
|
313
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* 07 (#204 ask 1): StringValueCStr runs a memchr over the name
|
|
317
|
+
* checking for embedded NULs — attribute names cannot contain
|
|
318
|
+
* NULs, so StringValue + RSTRING_PTR skips the scan. */
|
|
319
|
+
static VALUE nn_attribute(VALUE self, VALUE name)
|
|
320
|
+
{
|
|
321
|
+
struct native_node *n;
|
|
322
|
+
VALUE doc, cache, v;
|
|
323
|
+
const char *s;
|
|
324
|
+
|
|
325
|
+
/* String-normalized once, used as both the C argument and the
|
|
326
|
+
* cache key. */
|
|
327
|
+
if (!RB_TYPE_P(name, T_STRING))
|
|
328
|
+
name = rb_obj_as_string(name);
|
|
329
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
330
|
+
doc = n->document;
|
|
331
|
+
/* Single-slot memo first: the last-queried name by VALUE
|
|
332
|
+
* (callers may mint a fresh String per call) against the same
|
|
333
|
+
* version. Then the hash memo for other repeat names; both
|
|
334
|
+
* stamp with the document version, so any mutation through
|
|
335
|
+
* either surface drops them. Absent names never cache (no
|
|
336
|
+
* negative-cache staleness). */
|
|
337
|
+
if (doc != Qnil && n->attr_ver == rb_ivar_get(doc, id_iv_version) &&
|
|
338
|
+
n->attr_key != Qnil && rb_str_equal(n->attr_key, name))
|
|
339
|
+
return n->attr_val;
|
|
340
|
+
cache = Qnil;
|
|
341
|
+
if (doc != Qnil) {
|
|
342
|
+
cache = rb_ivar_get(self, id_iv_nn_attrs);
|
|
343
|
+
if (cache != Qnil &&
|
|
344
|
+
rb_ivar_get(self, id_iv_nn_attrs_ver) ==
|
|
345
|
+
rb_ivar_get(doc, id_iv_version)) {
|
|
346
|
+
v = rb_hash_aref(cache, name);
|
|
347
|
+
if (v != Qnil) {
|
|
348
|
+
n->attr_key = name;
|
|
349
|
+
n->attr_val = v;
|
|
350
|
+
n->attr_ver = rb_ivar_get(doc, id_iv_version);
|
|
351
|
+
return v;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
s = f_attr(n->ptr, RSTRING_PTR(name));
|
|
356
|
+
v = s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
357
|
+
if (doc != Qnil && v != Qnil) {
|
|
358
|
+
if (cache == Qnil)
|
|
359
|
+
cache = rb_hash_new();
|
|
360
|
+
rb_hash_aset(cache, name, v);
|
|
361
|
+
rb_ivar_set(self, id_iv_nn_attrs, cache);
|
|
362
|
+
rb_ivar_set(self, id_iv_nn_attrs_ver,
|
|
363
|
+
rb_ivar_get(doc, id_iv_version));
|
|
364
|
+
n->attr_key = name;
|
|
365
|
+
n->attr_val = v;
|
|
366
|
+
n->attr_ver = rb_ivar_get(doc, id_iv_version);
|
|
367
|
+
}
|
|
368
|
+
return v;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static VALUE nn_allocate(VALUE klass)
|
|
372
|
+
{
|
|
373
|
+
struct native_node *n;
|
|
374
|
+
return TypedData_Make_Struct(klass, struct native_node, &nn_type, n);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Klass-propagating reads (#246): a consumer subclass installed a
|
|
378
|
+
* per-kind child-klass map; reads mint that class so the child
|
|
379
|
+
* arrives already being the consumer's wrapper. Map-less callers
|
|
380
|
+
* (the base class) keep today's base mint. */
|
|
381
|
+
static VALUE klass_for_kind(VALUE owner_klass, int kind)
|
|
382
|
+
{
|
|
383
|
+
VALUE map = rb_attr_get(owner_klass, id_child_klasses);
|
|
384
|
+
VALUE k;
|
|
385
|
+
if (NIL_P(map))
|
|
386
|
+
return c_native_node;
|
|
387
|
+
k = rb_ary_entry(map, (long)kind);
|
|
388
|
+
return RB_TYPE_P(k, T_CLASS) ? k : c_native_node;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static VALUE nn_install_child_klasses(VALUE self, VALUE map)
|
|
392
|
+
{
|
|
393
|
+
long i, len;
|
|
394
|
+
Check_Type(map, T_ARRAY);
|
|
395
|
+
len = RARRAY_LEN(map);
|
|
396
|
+
if (len > 5)
|
|
397
|
+
rb_raise(rb_eArgError,
|
|
398
|
+
"child klasses: Array of classes/nil indexed by "
|
|
399
|
+
"node kind (element, text, comment, cdata, pi)");
|
|
400
|
+
for (i = 0; i < len; i++) {
|
|
401
|
+
VALUE k = rb_ary_entry(map, i);
|
|
402
|
+
if (!NIL_P(k) &&
|
|
403
|
+
!(RB_TYPE_P(k, T_CLASS) &&
|
|
404
|
+
RTEST(rb_class_inherited_p(k, c_native_node))))
|
|
405
|
+
rb_raise(rb_eTypeError,
|
|
406
|
+
"child klasses entries must be nil or a "
|
|
407
|
+
"Leptris::XML::NativeNode subclass");
|
|
408
|
+
}
|
|
409
|
+
rb_ivar_set(self, id_child_klasses, map);
|
|
410
|
+
return self;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Bulk children: one native pass; cache check/store inline; the
|
|
414
|
+
* document's wrapper_cache (a Ruby Hash keyed by address) provides
|
|
415
|
+
* identity for nodes seen through other paths. */
|
|
416
|
+
/* Two-call children fetch (#202): children_ex copies
|
|
417
|
+
* min(total, cap); a saturated fill means the list may be longer —
|
|
418
|
+
* query the true count (buf NULL) and refetch grown. */
|
|
419
|
+
static int fetch_children_two_call(void *node, void ***buf_out,
|
|
420
|
+
int **kinds_out)
|
|
421
|
+
{
|
|
422
|
+
size_t cap = 512;
|
|
423
|
+
void *b = ruby_xmalloc(cap * sizeof(void *));
|
|
424
|
+
int *k = ruby_xmalloc((int)cap * sizeof(int));
|
|
425
|
+
size_t count = f_children_ex(node, b, k, cap);
|
|
426
|
+
if (count == cap) {
|
|
427
|
+
size_t total = f_children_ex(node, NULL, NULL, 0);
|
|
428
|
+
if (total > cap) {
|
|
429
|
+
ruby_xfree(b);
|
|
430
|
+
ruby_xfree(k);
|
|
431
|
+
cap = total;
|
|
432
|
+
b = ruby_xmalloc(cap * sizeof(void *));
|
|
433
|
+
k = ruby_xmalloc((int)cap * sizeof(int));
|
|
434
|
+
count = f_children_ex(node, b, k, cap);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
*buf_out = b;
|
|
438
|
+
*kinds_out = k;
|
|
439
|
+
return (int)count;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
static VALUE nn_children(VALUE self)
|
|
443
|
+
{
|
|
444
|
+
struct native_node *n;
|
|
445
|
+
void **buf;
|
|
446
|
+
int *kinds;
|
|
447
|
+
int count, i;
|
|
448
|
+
VALUE cache, out, owner_klass;
|
|
449
|
+
|
|
450
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
451
|
+
count = fetch_children_two_call(n->ptr, &buf, &kinds);
|
|
452
|
+
if (count <= 0) {
|
|
453
|
+
ruby_xfree(buf);
|
|
454
|
+
ruby_xfree(kinds);
|
|
455
|
+
return rb_ary_new2(0);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
cache = native_cache_of(n->document);
|
|
459
|
+
out = rb_ary_new2(count);
|
|
460
|
+
owner_klass = rb_obj_class(self);
|
|
461
|
+
for (i = 0; i < count; i++) {
|
|
462
|
+
uint64_t addr = (uint64_t)(uintptr_t)buf[i];
|
|
463
|
+
VALUE key = ULL2NUM(addr);
|
|
464
|
+
VALUE child = rb_hash_aref(cache, key);
|
|
465
|
+
if (NIL_P(child)) {
|
|
466
|
+
struct native_node *cn;
|
|
467
|
+
child = nn_allocate(klass_for_kind(owner_klass, kinds[i]));
|
|
468
|
+
TypedData_Get_Struct(child, struct native_node, &nn_type, cn);
|
|
469
|
+
cn->ptr = buf[i];
|
|
470
|
+
cn->document = n->document;
|
|
471
|
+
rb_hash_aset(cache, key, child);
|
|
472
|
+
}
|
|
473
|
+
rb_ary_push(out, child);
|
|
474
|
+
}
|
|
475
|
+
ruby_xfree(buf);
|
|
476
|
+
ruby_xfree(kinds);
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
static VALUE nn_type_sym(VALUE self)
|
|
481
|
+
{
|
|
482
|
+
struct native_node *n;
|
|
483
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
484
|
+
switch (f_node_type(n->ptr)) {
|
|
485
|
+
case 0: return ID2SYM(rb_intern("element"));
|
|
486
|
+
case 1: return ID2SYM(rb_intern("text"));
|
|
487
|
+
case 2: return ID2SYM(rb_intern("comment"));
|
|
488
|
+
case 3: return ID2SYM(rb_intern("cdata"));
|
|
489
|
+
case 4: return ID2SYM(rb_intern("pi"));
|
|
490
|
+
default: return ID2SYM(rb_intern("node"));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Bulk children filtered to elements — the walk shape consumers
|
|
495
|
+
* actually iterate (#185's element_children parity). */
|
|
496
|
+
static VALUE nn_element_children(VALUE self)
|
|
497
|
+
{
|
|
498
|
+
struct native_node *n;
|
|
499
|
+
void **buf;
|
|
500
|
+
int *kinds;
|
|
501
|
+
int count, i;
|
|
502
|
+
VALUE cache, out, child_klass;
|
|
503
|
+
|
|
504
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
505
|
+
count = fetch_children_two_call(n->ptr, &buf, &kinds);
|
|
506
|
+
if (count <= 0) {
|
|
507
|
+
ruby_xfree(buf);
|
|
508
|
+
ruby_xfree(kinds);
|
|
509
|
+
return rb_ary_new2(0);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
cache = native_cache_of(n->document);
|
|
513
|
+
out = rb_ary_new();
|
|
514
|
+
child_klass = klass_for_kind(rb_obj_class(self), NT_ELEMENT);
|
|
515
|
+
for (i = 0; i < count; i++) {
|
|
516
|
+
if (kinds[i] != NT_ELEMENT)
|
|
517
|
+
continue;
|
|
518
|
+
VALUE key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
|
|
519
|
+
VALUE child = rb_hash_aref(cache, key);
|
|
520
|
+
if (NIL_P(child)) {
|
|
521
|
+
struct native_node *cn;
|
|
522
|
+
child = nn_allocate(child_klass);
|
|
523
|
+
TypedData_Get_Struct(child, struct native_node, &nn_type, cn);
|
|
524
|
+
cn->ptr = buf[i];
|
|
525
|
+
cn->document = n->document;
|
|
526
|
+
rb_hash_aset(cache, key, child);
|
|
527
|
+
}
|
|
528
|
+
rb_ary_push(out, child);
|
|
529
|
+
}
|
|
530
|
+
ruby_xfree(buf);
|
|
531
|
+
ruby_xfree(kinds);
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
static VALUE wrap_cached(VALUE owner_klass, struct native_node *owner,
|
|
536
|
+
void *ptr)
|
|
537
|
+
{
|
|
538
|
+
VALUE cache, key, node;
|
|
539
|
+
struct native_node *n;
|
|
540
|
+
|
|
541
|
+
cache = native_cache_of(owner->document);
|
|
542
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
543
|
+
node = rb_hash_aref(cache, key);
|
|
544
|
+
if (!NIL_P(node))
|
|
545
|
+
return node;
|
|
546
|
+
node = nn_allocate(klass_for_kind(owner_klass, f_node_type(ptr)));
|
|
547
|
+
TypedData_Get_Struct(node, struct native_node, &nn_type, n);
|
|
548
|
+
n->ptr = ptr;
|
|
549
|
+
n->document = owner->document;
|
|
550
|
+
rb_hash_aset(cache, key, node);
|
|
551
|
+
return node;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
static VALUE nn_next_sibling(VALUE self)
|
|
555
|
+
{
|
|
556
|
+
struct native_node *n;
|
|
557
|
+
void *sib;
|
|
558
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
559
|
+
sib = f_next_sibling(n->ptr);
|
|
560
|
+
return sib ? wrap_cached(rb_obj_class(self), n, sib) : Qnil;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
static VALUE nn_parent(VALUE self)
|
|
564
|
+
{
|
|
565
|
+
struct native_node *n;
|
|
566
|
+
void *par;
|
|
567
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
568
|
+
par = f_parent(n->ptr);
|
|
569
|
+
return par ? wrap_cached(rb_obj_class(self), n, par) : Qnil;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* Wrap a freshly-created C node as a NativeNode and register it
|
|
573
|
+
* in the document's identity cache. */
|
|
574
|
+
static VALUE wrap_new(VALUE document, void *ptr)
|
|
575
|
+
{
|
|
576
|
+
struct native_node *n;
|
|
577
|
+
VALUE node, cache, key;
|
|
578
|
+
|
|
579
|
+
node = nn_allocate(c_native_node);
|
|
580
|
+
TypedData_Get_Struct(node, struct native_node, &nn_type, n);
|
|
581
|
+
n->ptr = ptr;
|
|
582
|
+
n->document = document;
|
|
583
|
+
cache = native_cache_of(document);
|
|
584
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
585
|
+
rb_hash_aset(cache, key, node);
|
|
586
|
+
return node;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/* Document address from the binding Document's @c_address ivar
|
|
590
|
+
* (nil once freed) — one ivar read, no method dispatch. */
|
|
591
|
+
static void *doc_ptr_of(VALUE document)
|
|
592
|
+
{
|
|
593
|
+
VALUE addr = rb_ivar_get(document, id_iv_c_address);
|
|
594
|
+
if (NIL_P(addr))
|
|
595
|
+
rb_raise(rb_eRuntimeError, "document has been freed");
|
|
596
|
+
return (void *)(uintptr_t)NUM2ULL(addr);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/* Per-document identity caches. Both are lazily allocated on the
|
|
600
|
+
* Ruby side (@x ||= {}), so replicate that here: ivar read, create
|
|
601
|
+
* and store when unset. */
|
|
602
|
+
static VALUE native_cache_of(VALUE document)
|
|
603
|
+
{
|
|
604
|
+
VALUE cache = rb_ivar_get(document, id_iv_native_cache);
|
|
605
|
+
if (NIL_P(cache)) {
|
|
606
|
+
cache = rb_hash_new();
|
|
607
|
+
rb_ivar_set(document, id_iv_native_cache, cache);
|
|
608
|
+
}
|
|
609
|
+
return cache;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
static VALUE binding_cache_of(VALUE document)
|
|
613
|
+
{
|
|
614
|
+
VALUE cache = rb_ivar_get(document, id_iv_binding_cache);
|
|
615
|
+
if (NIL_P(cache)) {
|
|
616
|
+
cache = rb_hash_new();
|
|
617
|
+
rb_ivar_set(document, id_iv_binding_cache, cache);
|
|
618
|
+
}
|
|
619
|
+
return cache;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
/* TODO.perf/08 (#204 ask 2): native mutations must advance the
|
|
624
|
+
* binding Document's mutation version so binding memos invalidate
|
|
625
|
+
* exactly as they do through the FFI write path — and respect the
|
|
626
|
+
* readonly gate. Two ivar reads per mutation (the version is
|
|
627
|
+
* always a Fixnum, readonly is Qfalse/Qtrue). */
|
|
628
|
+
static VALUE c_readonly_error;
|
|
629
|
+
|
|
630
|
+
static void document_advance_version(VALUE document)
|
|
631
|
+
{
|
|
632
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
633
|
+
rb_raise(c_readonly_error,
|
|
634
|
+
"document is readonly — native mutation attempted");
|
|
635
|
+
rb_ivar_set(document, id_iv_version,
|
|
636
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
static VALUE nf_ns_lift_needed(VALUE self, VALUE addr)
|
|
640
|
+
{
|
|
641
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
642
|
+
const char *uri;
|
|
643
|
+
(void)self;
|
|
644
|
+
/* Exact semantics in two C calls: an element with NO resolved
|
|
645
|
+
* namespace and NO own declarations cannot lose anything in a
|
|
646
|
+
* move — bare name, no prefix binding, no default-ns
|
|
647
|
+
* dependency, nothing to prune. Everything else takes the full
|
|
648
|
+
* lift path (carries in-scope declarations, prunes redundant
|
|
649
|
+
* own ones). */
|
|
650
|
+
if (f_elem_ns_count(node) > 0)
|
|
651
|
+
return Qtrue;
|
|
652
|
+
uri = f_elem_ns(node);
|
|
653
|
+
return (uri && *uri) ? Qtrue : Qfalse;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* ---- C-bound append (TODO.perf/08-09, #204 ask 3) --------------
|
|
657
|
+
* One dispatch for the common programmatic-build adoption: the
|
|
658
|
+
* readonly + liveness gates, the provable no-op namespace-lift
|
|
659
|
+
* predicate, the version bump, and the engine append. Returns
|
|
660
|
+
* the engine status int; Qnil means the child DOES need the
|
|
661
|
+
* namespace lift — the caller falls back to the Ruby path. */
|
|
662
|
+
static VALUE nf_append_binding_child(VALUE self, VALUE document,
|
|
663
|
+
VALUE parent_addr, VALUE child_addr)
|
|
664
|
+
{
|
|
665
|
+
void *parent, *child;
|
|
666
|
+
const char *uri;
|
|
667
|
+
int st;
|
|
668
|
+
|
|
669
|
+
(void)self;
|
|
670
|
+
resolve_binding_classes();
|
|
671
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
672
|
+
rb_raise(c_use_after_free_error,
|
|
673
|
+
"owning document has been freed");
|
|
674
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
675
|
+
rb_raise(c_readonly_error,
|
|
676
|
+
"document is readonly — mutation attempted");
|
|
677
|
+
parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
678
|
+
child = (void *)(uintptr_t)NUM2ULL(child_addr);
|
|
679
|
+
/* Same provable no-op as Element.skip_adoption_lift?: an
|
|
680
|
+
* element with no resolved namespace and no own declarations
|
|
681
|
+
* cannot lose anything in a move; non-elements can never need
|
|
682
|
+
* a lift. */
|
|
683
|
+
if (f_node_type(child) == NT_ELEMENT) {
|
|
684
|
+
if (f_elem_ns_count(child) > 0)
|
|
685
|
+
return Qnil;
|
|
686
|
+
uri = f_elem_ns(child);
|
|
687
|
+
if (uri && *uri)
|
|
688
|
+
return Qnil;
|
|
689
|
+
}
|
|
690
|
+
/* Bump before the call, mirroring ensure_writable!: a failed
|
|
691
|
+
* mutation merely discards memos. */
|
|
692
|
+
rb_ivar_set(document, id_iv_version,
|
|
693
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
694
|
+
st = f_append_child(parent, child);
|
|
695
|
+
check_status_c(st);
|
|
696
|
+
return Qtrue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
|
|
700
|
+
* gates, the version bump (which drops the version-stamped
|
|
701
|
+
* attribute memos on both surfaces), and the engine write in one
|
|
702
|
+
* dispatch. */
|
|
703
|
+
static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
|
|
704
|
+
VALUE addr, VALUE name, VALUE value)
|
|
705
|
+
{
|
|
706
|
+
void *node;
|
|
707
|
+
int st;
|
|
708
|
+
|
|
709
|
+
(void)self;
|
|
710
|
+
resolve_binding_classes();
|
|
711
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
712
|
+
rb_raise(c_use_after_free_error,
|
|
713
|
+
"owning document has been freed");
|
|
714
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
715
|
+
rb_raise(c_readonly_error,
|
|
716
|
+
"document is readonly — mutation attempted");
|
|
717
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
718
|
+
rb_ivar_set(document, id_iv_version,
|
|
719
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
720
|
+
/* Attribute names cannot contain NULs (StringValueCStr's
|
|
721
|
+
* memchr is a wasted scan); VALUES keep the scan so an
|
|
722
|
+
* embedded-NUL value raises rather than truncates. */
|
|
723
|
+
st = f_set_attr(node, RSTRING_PTR(StringValue(name)),
|
|
724
|
+
StringValueCStr(value));
|
|
725
|
+
check_status_c(st);
|
|
726
|
+
return Qtrue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* C-bound value mutations (TODO.perf/23): the same gates + bump
|
|
730
|
+
* + engine write shape as set_binding_attribute. */
|
|
731
|
+
static VALUE nf_set_binding_name(VALUE self, VALUE document,
|
|
732
|
+
VALUE addr, VALUE name)
|
|
733
|
+
{
|
|
734
|
+
void *node;
|
|
735
|
+
int st;
|
|
736
|
+
|
|
737
|
+
(void)self;
|
|
738
|
+
resolve_binding_classes();
|
|
739
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
740
|
+
rb_raise(c_use_after_free_error,
|
|
741
|
+
"owning document has been freed");
|
|
742
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
743
|
+
rb_raise(c_readonly_error,
|
|
744
|
+
"document is readonly — mutation attempted");
|
|
745
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
746
|
+
rb_ivar_set(document, id_iv_version,
|
|
747
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
748
|
+
st = f_elem_set_name(node, StringValueCStr(name));
|
|
749
|
+
check_status_c(st);
|
|
750
|
+
return Qtrue;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* Element text or text-node content — kind-dispatched in C. */
|
|
754
|
+
static VALUE nf_set_binding_text(VALUE self, VALUE document,
|
|
755
|
+
VALUE addr, VALUE content)
|
|
756
|
+
{
|
|
757
|
+
void *node;
|
|
758
|
+
int st;
|
|
759
|
+
|
|
760
|
+
(void)self;
|
|
761
|
+
resolve_binding_classes();
|
|
762
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
763
|
+
rb_raise(c_use_after_free_error,
|
|
764
|
+
"owning document has been freed");
|
|
765
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
766
|
+
rb_raise(c_readonly_error,
|
|
767
|
+
"document is readonly — mutation attempted");
|
|
768
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
769
|
+
rb_ivar_set(document, id_iv_version,
|
|
770
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
771
|
+
st = f_node_type(node) == NT_ELEMENT
|
|
772
|
+
? f_elem_set_text(node, StringValueCStr(content))
|
|
773
|
+
: f_text_set_content(node, StringValueCStr(content));
|
|
774
|
+
check_status_c(st);
|
|
775
|
+
return Qtrue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
|
|
779
|
+
VALUE addr)
|
|
780
|
+
{
|
|
781
|
+
void *node;
|
|
782
|
+
int st;
|
|
783
|
+
|
|
784
|
+
(void)self;
|
|
785
|
+
resolve_binding_classes();
|
|
786
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
787
|
+
rb_raise(c_use_after_free_error,
|
|
788
|
+
"owning document has been freed");
|
|
789
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
790
|
+
rb_raise(c_readonly_error,
|
|
791
|
+
"document is readonly — mutation attempted");
|
|
792
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
793
|
+
rb_ivar_set(document, id_iv_version,
|
|
794
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
795
|
+
st = f_node_unlink(node);
|
|
796
|
+
check_status_c(st);
|
|
797
|
+
return Qtrue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/* C-bound root= (TODO.perf/33): gates + bump + engine set_root
|
|
801
|
+
* in one dispatch. The caller keeps the lift decision and the
|
|
802
|
+
* @root memo seed (wrap gives the right document association). */
|
|
803
|
+
static VALUE nf_set_binding_root(VALUE self, VALUE document,
|
|
804
|
+
VALUE addr)
|
|
805
|
+
{
|
|
806
|
+
void *node;
|
|
807
|
+
int st;
|
|
808
|
+
|
|
809
|
+
(void)self;
|
|
810
|
+
resolve_binding_classes();
|
|
811
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
812
|
+
rb_raise(c_use_after_free_error,
|
|
813
|
+
"owning document has been freed");
|
|
814
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
815
|
+
rb_raise(c_readonly_error,
|
|
816
|
+
"document is readonly — mutation attempted");
|
|
817
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
818
|
+
rb_ivar_set(document, id_iv_version,
|
|
819
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
820
|
+
st = f_set_root(doc_ptr_of(document), node);
|
|
821
|
+
check_status_c(st);
|
|
822
|
+
return Qtrue;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/* ---- C-yield traversal (TODO.perf/27) ----------------------------
|
|
826
|
+
* Same contracts as the Ruby FFI::Function versions without the
|
|
827
|
+
* per-call closure: traverse is post-order with abort-at-self
|
|
828
|
+
* (the receiver is the LAST node of its subtree in post-order,
|
|
829
|
+
* so stopping at self bounds the walk exactly) and
|
|
830
|
+
* stash-abort-raise exception discipline; visit passes (node,
|
|
831
|
+
* entering, depth). Walk state lives on the caller's C stack —
|
|
832
|
+
* nested traversals are independent. */
|
|
833
|
+
struct trav_state {
|
|
834
|
+
VALUE document;
|
|
835
|
+
VALUE err;
|
|
836
|
+
void *self_ptr;
|
|
837
|
+
int for_visit;
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
static VALUE trav_yield_one(VALUE arg)
|
|
841
|
+
{
|
|
842
|
+
return rb_yield(arg);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
static int trav_cb(void *node_ptr, void *user)
|
|
846
|
+
{
|
|
847
|
+
struct trav_state *st = user;
|
|
848
|
+
VALUE cache, key, node;
|
|
849
|
+
int kind, state;
|
|
850
|
+
|
|
851
|
+
if (st->err != Qnil)
|
|
852
|
+
return 1;
|
|
853
|
+
kind = f_node_type(node_ptr);
|
|
854
|
+
cache = binding_cache_of(st->document);
|
|
855
|
+
key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
|
|
856
|
+
node = rb_hash_aref(cache, key);
|
|
857
|
+
if (NIL_P(node)) {
|
|
858
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
859
|
+
rb_iv_set(node, "@c_address", key);
|
|
860
|
+
rb_iv_set(node, "@document", st->document);
|
|
861
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
862
|
+
if (rb_obj_class(st->document) == c_iteration_scope) {
|
|
863
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
864
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
865
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
866
|
+
} else {
|
|
867
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
868
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
869
|
+
rb_iv_set(node, "@pub_document", st->document);
|
|
870
|
+
}
|
|
871
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
872
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
873
|
+
rb_hash_aset(cache, key, node);
|
|
874
|
+
}
|
|
875
|
+
rb_protect(trav_yield_one, node, &state);
|
|
876
|
+
if (state) {
|
|
877
|
+
st->err = rb_errinfo();
|
|
878
|
+
return 1; /* abort the walk */
|
|
879
|
+
}
|
|
880
|
+
return node_ptr == st->self_ptr ? 1 : 0; /* abort-at-self */
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
static VALUE nf_traverse_binding(VALUE self, VALUE document,
|
|
884
|
+
VALUE addr)
|
|
885
|
+
{
|
|
886
|
+
struct trav_state st;
|
|
887
|
+
|
|
888
|
+
(void)self;
|
|
889
|
+
resolve_binding_classes();
|
|
890
|
+
st.document = document;
|
|
891
|
+
st.err = Qnil;
|
|
892
|
+
st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
|
|
893
|
+
st.for_visit = 0;
|
|
894
|
+
f_node_traverse(st.self_ptr, 1 /* TRAVERSE_POST_ORDER */,
|
|
895
|
+
trav_cb, &st);
|
|
896
|
+
if (st.err != Qnil)
|
|
897
|
+
rb_exc_raise(st.err);
|
|
898
|
+
return Qnil;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
struct visit_yield_args {
|
|
902
|
+
VALUE node, entering, depth;
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
static VALUE trav_yield_args(VALUE arg)
|
|
906
|
+
{
|
|
907
|
+
struct visit_yield_args *a = (struct visit_yield_args *)arg;
|
|
908
|
+
return rb_yield_values(3, a->node, a->entering, a->depth);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* Engine visit callback order is (user, node, entering, depth) —
|
|
912
|
+
* the Ruby closure's |_, node_ptr, entering, depth| mirrors it. */
|
|
913
|
+
static void visit_cb(void *user, void *node_ptr, int entering,
|
|
914
|
+
int depth)
|
|
915
|
+
{
|
|
916
|
+
/* visit's engine signature cannot abort; exceptions are
|
|
917
|
+
* stashed and every later node is skipped — the raise
|
|
918
|
+
* happens after the walk, same observable outcome. */
|
|
919
|
+
struct trav_state *st = user;
|
|
920
|
+
struct visit_yield_args va;
|
|
921
|
+
VALUE cache, key, node;
|
|
922
|
+
int kind, state;
|
|
923
|
+
|
|
924
|
+
if (st->err != Qnil)
|
|
925
|
+
return;
|
|
926
|
+
kind = f_node_type(node_ptr);
|
|
927
|
+
cache = binding_cache_of(st->document);
|
|
928
|
+
key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
|
|
929
|
+
node = rb_hash_aref(cache, key);
|
|
930
|
+
if (NIL_P(node)) {
|
|
931
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
932
|
+
rb_iv_set(node, "@c_address", key);
|
|
933
|
+
rb_iv_set(node, "@document", st->document);
|
|
934
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
935
|
+
if (rb_obj_class(st->document) == c_iteration_scope) {
|
|
936
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
937
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
938
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
939
|
+
} else {
|
|
940
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
941
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
942
|
+
rb_iv_set(node, "@pub_document", st->document);
|
|
943
|
+
}
|
|
944
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
945
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
946
|
+
rb_hash_aset(cache, key, node);
|
|
947
|
+
}
|
|
948
|
+
va.node = node;
|
|
949
|
+
va.entering = entering ? Qtrue : Qfalse;
|
|
950
|
+
va.depth = INT2NUM(depth);
|
|
951
|
+
rb_protect(trav_yield_args, (VALUE)&va, &state);
|
|
952
|
+
if (state)
|
|
953
|
+
st->err = rb_errinfo();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
static VALUE nf_visit_binding(VALUE self, VALUE document, VALUE addr)
|
|
957
|
+
{
|
|
958
|
+
struct trav_state st;
|
|
959
|
+
|
|
960
|
+
(void)self;
|
|
961
|
+
resolve_binding_classes();
|
|
962
|
+
st.document = document;
|
|
963
|
+
st.err = Qnil;
|
|
964
|
+
st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
|
|
965
|
+
st.for_visit = 1;
|
|
966
|
+
f_node_visit(st.self_ptr, visit_cb, &st);
|
|
967
|
+
if (st.err != Qnil)
|
|
968
|
+
rb_exc_raise(st.err);
|
|
969
|
+
return Qnil;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/* ---- Address-based fills (TODO.perf/28) -------------------------
|
|
973
|
+
* Pure reads of node-local data — no cache, no version — the
|
|
974
|
+
* same shapes the inner_html pass uses. */
|
|
975
|
+
static VALUE nf_fast_text_content(VALUE self, VALUE addr)
|
|
976
|
+
{
|
|
977
|
+
const char *s;
|
|
978
|
+
(void)self;
|
|
979
|
+
s = f_text_content((void *)(uintptr_t)NUM2ULL(addr));
|
|
980
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
static VALUE nf_fast_comment_content(VALUE self, VALUE addr)
|
|
984
|
+
{
|
|
985
|
+
const char *s;
|
|
986
|
+
(void)self;
|
|
987
|
+
s = f_comment_content((void *)(uintptr_t)NUM2ULL(addr));
|
|
988
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
static VALUE nf_fast_cdata_content(VALUE self, VALUE addr)
|
|
992
|
+
{
|
|
993
|
+
const char *s;
|
|
994
|
+
(void)self;
|
|
995
|
+
s = f_cdata_content((void *)(uintptr_t)NUM2ULL(addr));
|
|
996
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
static VALUE nf_fast_pi_target(VALUE self, VALUE addr)
|
|
1000
|
+
{
|
|
1001
|
+
const char *s;
|
|
1002
|
+
(void)self;
|
|
1003
|
+
s = f_pi_target((void *)(uintptr_t)NUM2ULL(addr));
|
|
1004
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
static VALUE nf_fast_pi_data(VALUE self, VALUE addr)
|
|
1008
|
+
{
|
|
1009
|
+
const char *s;
|
|
1010
|
+
(void)self;
|
|
1011
|
+
s = f_pi_data((void *)(uintptr_t)NUM2ULL(addr));
|
|
1012
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/* Shared construction for address-walk faces (TODO.perf/30):
|
|
1016
|
+
* cache lookup + kind-dispatched binding wrapper. */
|
|
1017
|
+
static VALUE wrap_addr_node(VALUE document, void *ptr)
|
|
1018
|
+
{
|
|
1019
|
+
VALUE cache, key, node;
|
|
1020
|
+
int kind;
|
|
1021
|
+
|
|
1022
|
+
kind = f_node_type(ptr);
|
|
1023
|
+
cache = binding_cache_of(document);
|
|
1024
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
1025
|
+
node = rb_hash_aref(cache, key);
|
|
1026
|
+
if (NIL_P(node)) {
|
|
1027
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
1028
|
+
rb_iv_set(node, "@c_address", key);
|
|
1029
|
+
rb_iv_set(node, "@document", document);
|
|
1030
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
1031
|
+
if (rb_obj_class(document) == c_iteration_scope) {
|
|
1032
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
1033
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
1034
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
1035
|
+
} else {
|
|
1036
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1037
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1038
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1039
|
+
}
|
|
1040
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
1041
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
1042
|
+
rb_hash_aset(cache, key, node);
|
|
1043
|
+
}
|
|
1044
|
+
return node;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/* First element child in one C walk (text-heavy parents paid two
|
|
1048
|
+
* FFI calls per skipped sibling). */
|
|
1049
|
+
static VALUE nf_first_element_child(VALUE self, VALUE document,
|
|
1050
|
+
VALUE addr)
|
|
1051
|
+
{
|
|
1052
|
+
void *p;
|
|
1053
|
+
|
|
1054
|
+
(void)self;
|
|
1055
|
+
resolve_binding_classes();
|
|
1056
|
+
for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
|
|
1057
|
+
p != NULL; p = f_next_sibling(p)) {
|
|
1058
|
+
if (f_node_type(p) == NT_ELEMENT)
|
|
1059
|
+
return wrap_addr_node(document, p);
|
|
1060
|
+
}
|
|
1061
|
+
return Qnil;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/* Last element child in one C walk (the batch fetch materialized
|
|
1065
|
+
* every child to keep one). */
|
|
1066
|
+
static VALUE nf_last_element_child(VALUE self, VALUE document,
|
|
1067
|
+
VALUE addr)
|
|
1068
|
+
{
|
|
1069
|
+
void *p, *last = NULL;
|
|
1070
|
+
|
|
1071
|
+
(void)self;
|
|
1072
|
+
resolve_binding_classes();
|
|
1073
|
+
for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
|
|
1074
|
+
p != NULL; p = f_next_sibling(p)) {
|
|
1075
|
+
if (f_node_type(p) == NT_ELEMENT)
|
|
1076
|
+
last = p;
|
|
1077
|
+
}
|
|
1078
|
+
return last ? wrap_addr_node(document, last) : Qnil;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/* Owned string: copy + free through the engine's seam (the
|
|
1082
|
+
* read_owned_string protocol). */
|
|
1083
|
+
static VALUE nf_fast_path(VALUE self, VALUE addr)
|
|
1084
|
+
{
|
|
1085
|
+
const char *s;
|
|
1086
|
+
VALUE out;
|
|
1087
|
+
(void)self;
|
|
1088
|
+
s = f_node_xpath((void *)(uintptr_t)NUM2ULL(addr));
|
|
1089
|
+
if (!s)
|
|
1090
|
+
return Qnil;
|
|
1091
|
+
out = rb_utf8_str_new_cstr(s);
|
|
1092
|
+
f_free_str((void *)s);
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/* C-bound insert family (TODO.perf/14): prepend / insert-after /
|
|
1097
|
+
* insert-before share append's gates + predicate + bump shape.
|
|
1098
|
+
* mode: 1 prepend, 2 after, 3 before (anchor = receiver). */
|
|
1099
|
+
static VALUE nf_insert_binding_child(VALUE self, VALUE document,
|
|
1100
|
+
VALUE anchor_addr, VALUE child_addr,
|
|
1101
|
+
VALUE mode)
|
|
1102
|
+
{
|
|
1103
|
+
void *anchor, *child;
|
|
1104
|
+
const char *uri;
|
|
1105
|
+
int st, m;
|
|
1106
|
+
|
|
1107
|
+
(void)self;
|
|
1108
|
+
resolve_binding_classes();
|
|
1109
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
1110
|
+
rb_raise(c_use_after_free_error,
|
|
1111
|
+
"owning document has been freed");
|
|
1112
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
1113
|
+
rb_raise(c_readonly_error,
|
|
1114
|
+
"document is readonly — mutation attempted");
|
|
1115
|
+
anchor = (void *)(uintptr_t)NUM2ULL(anchor_addr);
|
|
1116
|
+
child = (void *)(uintptr_t)NUM2ULL(child_addr);
|
|
1117
|
+
m = FIX2INT(mode);
|
|
1118
|
+
if (f_node_type(child) == NT_ELEMENT) {
|
|
1119
|
+
if (f_elem_ns_count(child) > 0)
|
|
1120
|
+
return Qnil;
|
|
1121
|
+
uri = f_elem_ns(child);
|
|
1122
|
+
if (uri && *uri)
|
|
1123
|
+
return Qnil;
|
|
1124
|
+
}
|
|
1125
|
+
rb_ivar_set(document, id_iv_version,
|
|
1126
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
1127
|
+
switch (m) {
|
|
1128
|
+
case 1: st = f_prepend_child(anchor, child); break;
|
|
1129
|
+
case 2: st = f_insert_after(anchor, child); break;
|
|
1130
|
+
case 3: st = f_insert_before(anchor, child); break;
|
|
1131
|
+
default:
|
|
1132
|
+
rb_raise(rb_eArgError, "invalid insert mode %d", m);
|
|
1133
|
+
return Qnil;
|
|
1134
|
+
}
|
|
1135
|
+
check_status_c(st);
|
|
1136
|
+
return Qtrue;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/* Builder factories (#149): create in C, wrap as NativeNode — no
|
|
1140
|
+
* FFI::Pointer, no Ruby wrap_fresh path. */
|
|
1141
|
+
static VALUE nn_create_element(VALUE klass, VALUE document, VALUE name)
|
|
1142
|
+
{
|
|
1143
|
+
void *ptr;
|
|
1144
|
+
(void)klass;
|
|
1145
|
+
ptr = f_elem_create(doc_ptr_of(document), StringValueCStr(name));
|
|
1146
|
+
if (!ptr)
|
|
1147
|
+
rb_raise(rb_eRuntimeError, "leptris_element_create failed");
|
|
1148
|
+
return wrap_new(document, ptr);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
static VALUE nn_create_text(VALUE klass, VALUE document, VALUE content)
|
|
1152
|
+
{
|
|
1153
|
+
void *ptr;
|
|
1154
|
+
(void)klass;
|
|
1155
|
+
ptr = f_text_create(doc_ptr_of(document), StringValueCStr(content));
|
|
1156
|
+
if (!ptr)
|
|
1157
|
+
rb_raise(rb_eRuntimeError, "leptris_text_node_create failed");
|
|
1158
|
+
return wrap_new(document, ptr);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
static VALUE nn_create_child(VALUE self, VALUE name)
|
|
1162
|
+
{
|
|
1163
|
+
struct native_node *n;
|
|
1164
|
+
void *ptr;
|
|
1165
|
+
VALUE out;
|
|
1166
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1167
|
+
document_advance_version(n->document);
|
|
1168
|
+
ptr = f_create_child(n->ptr, StringValueCStr(name));
|
|
1169
|
+
if (!ptr)
|
|
1170
|
+
rb_raise(rb_eRuntimeError, "leptris_element_create_child failed");
|
|
1171
|
+
out = wrap_new(n->document, ptr);
|
|
1172
|
+
return out;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
static VALUE nn_append_child(VALUE self, VALUE child)
|
|
1176
|
+
{
|
|
1177
|
+
struct native_node *n, *c;
|
|
1178
|
+
int rc;
|
|
1179
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1180
|
+
TypedData_Get_Struct(child, struct native_node, &nn_type, c);
|
|
1181
|
+
document_advance_version(n->document);
|
|
1182
|
+
rc = f_append_child(n->ptr, c->ptr);
|
|
1183
|
+
if (rc != 0)
|
|
1184
|
+
rb_raise(rb_eRuntimeError, "leptris_element_append_child failed (%d)", rc);
|
|
1185
|
+
return child;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
static VALUE nn_set_root(VALUE klass, VALUE document, VALUE element)
|
|
1189
|
+
{
|
|
1190
|
+
struct native_node *n;
|
|
1191
|
+
int rc;
|
|
1192
|
+
(void)klass;
|
|
1193
|
+
TypedData_Get_Struct(element, struct native_node, &nn_type, n);
|
|
1194
|
+
document_advance_version(document);
|
|
1195
|
+
rc = f_set_root(doc_ptr_of(document), n->ptr);
|
|
1196
|
+
if (rc != 0)
|
|
1197
|
+
rb_raise(rb_eRuntimeError, "leptris_document_set_root failed (%d)", rc);
|
|
1198
|
+
return element;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* NativeNode surface completion (TODO.perf/05): bulk attribute
|
|
1202
|
+
* hash (name => value, one C walk, no per-attr Ruby frames) and
|
|
1203
|
+
* the position readers. */
|
|
1204
|
+
static VALUE nn_attributes(VALUE self)
|
|
1205
|
+
{
|
|
1206
|
+
struct native_node *n;
|
|
1207
|
+
VALUE hash;
|
|
1208
|
+
void *attr;
|
|
1209
|
+
|
|
1210
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1211
|
+
hash = rb_hash_new();
|
|
1212
|
+
attr = f_attr_first(n->ptr);
|
|
1213
|
+
while (attr) {
|
|
1214
|
+
const char *name = f_attr_name(attr);
|
|
1215
|
+
const char *value = f_attr_value(n->ptr, attr);
|
|
1216
|
+
rb_hash_aset(hash,
|
|
1217
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil,
|
|
1218
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
1219
|
+
attr = f_attr_next(attr);
|
|
1220
|
+
}
|
|
1221
|
+
return hash;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
static VALUE nn_line(VALUE self)
|
|
1225
|
+
{
|
|
1226
|
+
struct native_node *n;
|
|
1227
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1228
|
+
return UINT2NUM(f_node_line(n->ptr));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
static VALUE nn_byte_offset(VALUE self)
|
|
1232
|
+
{
|
|
1233
|
+
struct native_node *n;
|
|
1234
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1235
|
+
return ULL2NUM((uint64_t)f_node_offset(n->ptr));
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/* The owning binding Document (moxml #213: adapters route
|
|
1239
|
+
* native.document exactly like Node#document). */
|
|
1240
|
+
static VALUE nn_document(VALUE self)
|
|
1241
|
+
{
|
|
1242
|
+
struct native_node *n;
|
|
1243
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1244
|
+
return n->document;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
static VALUE nn_address(VALUE self)
|
|
1248
|
+
{
|
|
1249
|
+
struct native_node *n;
|
|
1250
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
1251
|
+
return ULL2NUM((uint64_t)(uintptr_t)n->ptr);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/* ---- Bulk children for BINDING wrappers (TODO.perf/01 tail) ----
|
|
1255
|
+
* One C pass: children_ex + binding-class dispatch + ivar set +
|
|
1256
|
+
* identity-cache check/store. Returns a Ruby Array of Element/
|
|
1257
|
+
* Text/... wrappers — Node#children swaps this in when the ext
|
|
1258
|
+
* is loaded. Class constants and FFI::Pointer resolve once at
|
|
1259
|
+
* Init; the per-node Ruby frames (wrap/construct/memo/alive)
|
|
1260
|
+
* disappear. */
|
|
1261
|
+
static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
|
|
1262
|
+
c_b_pi, c_b_entity_ref, c_b_node, c_b_result_text,
|
|
1263
|
+
c_b_result_attr,
|
|
1264
|
+
c_b_attr, c_ffi_pointer, c_b_document, c_b_freed;
|
|
1265
|
+
static VALUE c_iteration_scope;
|
|
1266
|
+
static ID id_ptr_new;
|
|
1267
|
+
|
|
1268
|
+
/* Binding classes resolve LAZILY: Init_native can run before the
|
|
1269
|
+
* binding's autoload entries load (require "leptris" flow) —
|
|
1270
|
+
* rb_path2class at Init time raises for undefined constants. */
|
|
1271
|
+
static void resolve_binding_classes(void)
|
|
1272
|
+
{
|
|
1273
|
+
if (c_b_element != Qundef)
|
|
1274
|
+
return;
|
|
1275
|
+
c_b_element = rb_path2class("Leptris::XML::Element");
|
|
1276
|
+
c_b_text = rb_path2class("Leptris::XML::Text");
|
|
1277
|
+
c_b_comment = rb_path2class("Leptris::XML::Comment");
|
|
1278
|
+
c_b_cdata = rb_path2class("Leptris::XML::CDATA");
|
|
1279
|
+
c_b_pi = rb_path2class("Leptris::XML::ProcessingInstruction");
|
|
1280
|
+
c_b_entity_ref = rb_path2class("Leptris::XML::EntityReference");
|
|
1281
|
+
c_b_node = rb_path2class("Leptris::XML::Node");
|
|
1282
|
+
c_b_result_text = rb_path2class("Leptris::XML::ResultText");
|
|
1283
|
+
c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
|
|
1284
|
+
c_ffi_pointer = rb_path2class("FFI::Pointer");
|
|
1285
|
+
c_b_attr = rb_path2class("Leptris::XML::Attr");
|
|
1286
|
+
c_b_document = rb_path2class("Leptris::XML::Document");
|
|
1287
|
+
c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
|
|
1288
|
+
c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
|
|
1289
|
+
c_leptris_error = rb_path2class("Leptris::XML::Error");
|
|
1290
|
+
id_ptr_new = rb_intern("new");
|
|
1291
|
+
rb_gc_register_mark_object(c_b_element);
|
|
1292
|
+
rb_gc_register_mark_object(c_b_text);
|
|
1293
|
+
rb_gc_register_mark_object(c_b_comment);
|
|
1294
|
+
rb_gc_register_mark_object(c_b_cdata);
|
|
1295
|
+
rb_gc_register_mark_object(c_b_pi);
|
|
1296
|
+
rb_gc_register_mark_object(c_b_entity_ref);
|
|
1297
|
+
rb_gc_register_mark_object(c_b_node);
|
|
1298
|
+
rb_gc_register_mark_object(c_b_result_text);
|
|
1299
|
+
rb_gc_register_mark_object(c_b_result_attr);
|
|
1300
|
+
rb_gc_register_mark_object(c_ffi_pointer);
|
|
1301
|
+
rb_gc_register_mark_object(c_b_attr);
|
|
1302
|
+
rb_gc_register_mark_object(c_b_document);
|
|
1303
|
+
rb_gc_register_mark_object(c_b_freed);
|
|
1304
|
+
rb_gc_register_mark_object(c_iteration_scope);
|
|
1305
|
+
rb_gc_register_mark_object(c_leptris_error);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
static VALUE binding_klass_for(int kind)
|
|
1309
|
+
{
|
|
1310
|
+
switch (kind) {
|
|
1311
|
+
case 0: return c_b_element;
|
|
1312
|
+
case 1: return c_b_text;
|
|
1313
|
+
case 2: return c_b_comment;
|
|
1314
|
+
case 3: return c_b_cdata;
|
|
1315
|
+
case 4: return c_b_pi;
|
|
1316
|
+
case 10: return c_b_entity_ref; /* #212 / upstream #1094 */
|
|
1317
|
+
default: return c_b_node;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
1322
|
+
int elements_only)
|
|
1323
|
+
{
|
|
1324
|
+
void **buf;
|
|
1325
|
+
int *kinds;
|
|
1326
|
+
int count, i;
|
|
1327
|
+
VALUE cache, out;
|
|
1328
|
+
|
|
1329
|
+
count = fetch_children_two_call((void *)(uintptr_t)NUM2ULL(parent_addr),
|
|
1330
|
+
&buf, &kinds);
|
|
1331
|
+
if (count <= 0) {
|
|
1332
|
+
ruby_xfree(buf);
|
|
1333
|
+
ruby_xfree(kinds);
|
|
1334
|
+
return rb_ary_new2(0);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
cache = binding_cache_of(document);
|
|
1338
|
+
out = rb_ary_new2(elements_only ? 8 : count);
|
|
1339
|
+
for (i = 0; i < count; i++) {
|
|
1340
|
+
VALUE key, node;
|
|
1341
|
+
if (elements_only && kinds[i] != 0)
|
|
1342
|
+
continue;
|
|
1343
|
+
key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
|
|
1344
|
+
node = rb_hash_aref(cache, key);
|
|
1345
|
+
if (NIL_P(node)) {
|
|
1346
|
+
node = rb_obj_alloc(binding_klass_for(kinds[i]));
|
|
1347
|
+
rb_iv_set(node, "@c_address", key);
|
|
1348
|
+
rb_iv_set(node, "@document", document);
|
|
1349
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
1350
|
+
/* TODO.perf/25: scope-aware stamps — the IterationScope
|
|
1351
|
+
* flows through here as "document" for iterparse
|
|
1352
|
+
* elements; those stay memo-exempt and FFI-mutating,
|
|
1353
|
+
* but address-based reads are cache-free and safe. */
|
|
1354
|
+
if (rb_obj_class(document) == c_iteration_scope) {
|
|
1355
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
1356
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
1357
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
1358
|
+
} else {
|
|
1359
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1360
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1361
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1362
|
+
}
|
|
1363
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
1364
|
+
rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
|
|
1365
|
+
rb_hash_aset(cache, key, node);
|
|
1366
|
+
}
|
|
1367
|
+
rb_ary_push(out, node);
|
|
1368
|
+
}
|
|
1369
|
+
ruby_xfree(buf);
|
|
1370
|
+
ruby_xfree(kinds);
|
|
1371
|
+
return out;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
static VALUE nf_bulk_children(VALUE self, VALUE document, VALUE parent_addr)
|
|
1375
|
+
{
|
|
1376
|
+
(void)self;
|
|
1377
|
+
resolve_binding_classes();
|
|
1378
|
+
return bulk_children_impl(document, parent_addr, 0);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
static VALUE nf_bulk_element_children(VALUE self, VALUE document,
|
|
1382
|
+
VALUE parent_addr)
|
|
1383
|
+
{
|
|
1384
|
+
(void)self;
|
|
1385
|
+
resolve_binding_classes();
|
|
1386
|
+
return bulk_children_impl(document, parent_addr, 1);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/* ---- Bulk XPath result materialization (TODO.perf/03) ----------
|
|
1390
|
+
* One C pass over leptris_xpath_result_get_nodes_ex: binding-class
|
|
1391
|
+
* dispatch (XPATH_NODE space: 0 element, 1 attribute, 2 text,
|
|
1392
|
+
* 3 other), value capture for synthetic text/attribute items
|
|
1393
|
+
* while the result handle is alive, identity-cache check/store
|
|
1394
|
+
* for elements. Returns the Ruby Array. */
|
|
1395
|
+
static VALUE m_native_sentinel; /* module object = fallback marker */
|
|
1396
|
+
static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
1397
|
+
void *result, void *ptr, int kind,
|
|
1398
|
+
int idx);
|
|
1399
|
+
static VALUE binding_klass_for(int kind);
|
|
1400
|
+
static VALUE c_iteration_scope;
|
|
1401
|
+
|
|
1402
|
+
static VALUE bulk_xpath_array(VALUE document, void *result);
|
|
1403
|
+
|
|
1404
|
+
static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
1405
|
+
{
|
|
1406
|
+
(void)self;
|
|
1407
|
+
return bulk_xpath_array(document,
|
|
1408
|
+
(void *)(uintptr_t)NUM2ULL(result_ptr_val));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/* TODO.perf/22: materialize AND free — the eager xpath path owns
|
|
1412
|
+
* the handle lifecycle in C; no Ruby AutoPointer, no finalizer. */
|
|
1413
|
+
static VALUE nf_materialize_xpath(VALUE self, VALUE document,
|
|
1414
|
+
VALUE result_ptr_val)
|
|
1415
|
+
{
|
|
1416
|
+
void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
|
|
1417
|
+
VALUE out;
|
|
1418
|
+
long i, n;
|
|
1419
|
+
|
|
1420
|
+
(void)self;
|
|
1421
|
+
out = bulk_xpath_array(document, result);
|
|
1422
|
+
n = RARRAY_LEN(out);
|
|
1423
|
+
for (i = 0; i < n; i++) {
|
|
1424
|
+
if (rb_ary_entry(out, i) == m_native_sentinel) {
|
|
1425
|
+
/* Exotic kinds need the Ruby fallback per entry — the
|
|
1426
|
+
* caller keeps the handle and goes the lazy path. */
|
|
1427
|
+
return Qnil;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
f_xp_free(result);
|
|
1431
|
+
return out;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
static VALUE bulk_xpath_array(VALUE document, void *result)
|
|
1435
|
+
{
|
|
1436
|
+
void **buf;
|
|
1437
|
+
int *kinds;
|
|
1438
|
+
int count, i;
|
|
1439
|
+
VALUE cache, out;
|
|
1440
|
+
|
|
1441
|
+
resolve_binding_classes();
|
|
1442
|
+
size_t scount = f_xp_count(result);
|
|
1443
|
+
if (scount == 0)
|
|
1444
|
+
return rb_ary_new2(0);
|
|
1445
|
+
|
|
1446
|
+
buf = ruby_xmalloc(scount * sizeof(void *));
|
|
1447
|
+
kinds = ruby_xmalloc(scount * sizeof(int));
|
|
1448
|
+
scount = f_xp_nodes_ex(result, buf, kinds, scount);
|
|
1449
|
+
count = (int)scount;
|
|
1450
|
+
if (count <= 0) {
|
|
1451
|
+
ruby_xfree(buf);
|
|
1452
|
+
ruby_xfree(kinds);
|
|
1453
|
+
return rb_ary_new2(0);
|
|
1454
|
+
}
|
|
1455
|
+
cache = binding_cache_of(document);
|
|
1456
|
+
out = rb_ary_new2(count);
|
|
1457
|
+
for (i = 0; i < count; i++) {
|
|
1458
|
+
if (buf[i] == NULL)
|
|
1459
|
+
continue;
|
|
1460
|
+
rb_ary_push(out, materialize_xp_entry(document, cache, result,
|
|
1461
|
+
buf[i], kinds[i], i));
|
|
1462
|
+
}
|
|
1463
|
+
ruby_xfree(buf);
|
|
1464
|
+
ruby_xfree(kinds);
|
|
1465
|
+
return out;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/* Materializes ONE result entry as a binding wrapper (or the
|
|
1469
|
+
* module sentinel for the rare kinds the bulk path defers to
|
|
1470
|
+
* Ruby). Shared by the bulk materializer (TODO.perf/03) and the
|
|
1471
|
+
* at_xpath single-result seam (TODO.perf/16). */
|
|
1472
|
+
static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
1473
|
+
void *result, void *ptr, int kind,
|
|
1474
|
+
int idx)
|
|
1475
|
+
{
|
|
1476
|
+
VALUE node, key;
|
|
1477
|
+
const char *s;
|
|
1478
|
+
|
|
1479
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
1480
|
+
switch (kind) {
|
|
1481
|
+
case 0: /* element */
|
|
1482
|
+
node = rb_hash_aref(cache, key);
|
|
1483
|
+
if (NIL_P(node)) {
|
|
1484
|
+
node = rb_obj_alloc(c_b_element);
|
|
1485
|
+
rb_iv_set(node, "@c_address", key);
|
|
1486
|
+
rb_iv_set(node, "@document", document);
|
|
1487
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
1488
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1489
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1490
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1491
|
+
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
1492
|
+
rb_hash_aset(cache, key, node);
|
|
1493
|
+
}
|
|
1494
|
+
return node;
|
|
1495
|
+
case 1: /* synthetic attribute */
|
|
1496
|
+
{
|
|
1497
|
+
const char *name = f_xp_node_name(result, idx);
|
|
1498
|
+
const char *value = f_xp_node_value(result, idx);
|
|
1499
|
+
node = rb_obj_alloc(c_b_result_attr);
|
|
1500
|
+
rb_iv_set(node, "@c_address", key);
|
|
1501
|
+
rb_iv_set(node, "@document", document);
|
|
1502
|
+
rb_iv_set(node, "@attr_name",
|
|
1503
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil);
|
|
1504
|
+
rb_iv_set(node, "@attr_value",
|
|
1505
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
1506
|
+
}
|
|
1507
|
+
return node;
|
|
1508
|
+
case 2: /* text-kind: synthetic sequence carrier, or a real
|
|
1509
|
+
* Text/CDATA node (XPath reports both as TEXT) —
|
|
1510
|
+
* dispatch on the node's own type like Node.wrap. */
|
|
1511
|
+
{
|
|
1512
|
+
int nt = f_node_type(ptr);
|
|
1513
|
+
if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
|
|
1514
|
+
s = f_xp_node_value(result, idx);
|
|
1515
|
+
node = rb_obj_alloc(c_b_result_text);
|
|
1516
|
+
rb_iv_set(node, "@c_address", key);
|
|
1517
|
+
rb_iv_set(node, "@document", document);
|
|
1518
|
+
rb_iv_set(node, "@value",
|
|
1519
|
+
s ? rb_utf8_str_new_cstr(s) : Qnil);
|
|
1520
|
+
return node;
|
|
1521
|
+
}
|
|
1522
|
+
node = rb_hash_aref(cache, key);
|
|
1523
|
+
if (NIL_P(node)) {
|
|
1524
|
+
node = rb_obj_alloc(binding_klass_for(nt));
|
|
1525
|
+
rb_iv_set(node, "@c_address", key);
|
|
1526
|
+
rb_iv_set(node, "@document", document);
|
|
1527
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
1528
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1529
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1530
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1531
|
+
rb_iv_set(node, "@node_type", INT2FIX(nt));
|
|
1532
|
+
rb_hash_aset(cache, key, node);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return node;
|
|
1536
|
+
default: /* other: rare — signal Ruby fallback for this entry */
|
|
1537
|
+
return m_native_sentinel;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/* ---- at_xpath single-result seam (TODO.perf/16) ----------------
|
|
1542
|
+
* One dispatch for the hottest query shape: type check, entry-0
|
|
1543
|
+
* materialization through the bulk machinery, result free.
|
|
1544
|
+
* Non-nodeset results return Qundef — the caller keeps the exact
|
|
1545
|
+
* Ruby scalar path (and owns the free there). */
|
|
1546
|
+
static VALUE nf_at_xpath_first(VALUE self, VALUE document,
|
|
1547
|
+
VALUE result_ptr_val)
|
|
1548
|
+
{
|
|
1549
|
+
void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
|
|
1550
|
+
void *buf;
|
|
1551
|
+
int kind;
|
|
1552
|
+
size_t scount;
|
|
1553
|
+
VALUE node;
|
|
1554
|
+
|
|
1555
|
+
(void)self;
|
|
1556
|
+
resolve_binding_classes();
|
|
1557
|
+
if (f_xp_type(result) != 0) /* XPATH_NODESET */
|
|
1558
|
+
return m_native_sentinel; /* caller keeps the scalar path */
|
|
1559
|
+
scount = f_xp_count(result);
|
|
1560
|
+
if (scount == 0) {
|
|
1561
|
+
f_xp_free(result);
|
|
1562
|
+
return Qnil;
|
|
1563
|
+
}
|
|
1564
|
+
buf = NULL;
|
|
1565
|
+
scount = f_xp_nodes_ex(result, &buf, &kind, 1);
|
|
1566
|
+
if (scount == 0 || buf == NULL) {
|
|
1567
|
+
f_xp_free(result);
|
|
1568
|
+
return Qnil;
|
|
1569
|
+
}
|
|
1570
|
+
node = materialize_xp_entry(document, binding_cache_of(document),
|
|
1571
|
+
result, buf, kind, 0);
|
|
1572
|
+
f_xp_free(result);
|
|
1573
|
+
return node;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/* ---- Bulk attribute materialization (TODO.perf/10) ---------------
|
|
1577
|
+
* Element#attributes / #keys / #values walk the attribute list
|
|
1578
|
+
* one FFI call per row (name + value). One C pass builds the
|
|
1579
|
+
* {name => value} hash for the binding. */
|
|
1580
|
+
static VALUE nf_bulk_attributes(VALUE self, VALUE addr)
|
|
1581
|
+
{
|
|
1582
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
1583
|
+
VALUE hash = rb_hash_new();
|
|
1584
|
+
void *attr = f_attr_first(node);
|
|
1585
|
+
|
|
1586
|
+
(void)self;
|
|
1587
|
+
while (attr) {
|
|
1588
|
+
const char *name = f_attr_name(attr);
|
|
1589
|
+
const char *value = f_attr_value(node, attr);
|
|
1590
|
+
rb_hash_aset(hash,
|
|
1591
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil,
|
|
1592
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
1593
|
+
attr = f_attr_next(attr);
|
|
1594
|
+
}
|
|
1595
|
+
return hash;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/* ---- Binding create via C (TODO.perf/09): one call creates the
|
|
1599
|
+
* node AND constructs the binding wrapper (class dispatch, ivars,
|
|
1600
|
+
* identity-cache store) — no FFI marshaling, no wrap_fresh path. */
|
|
1601
|
+
static VALUE nf_create_binding_element(VALUE self, VALUE document,
|
|
1602
|
+
VALUE name)
|
|
1603
|
+
{
|
|
1604
|
+
void *doc = doc_ptr_of(document);
|
|
1605
|
+
void *ptr = f_elem_create(doc, RSTRING_PTR(StringValue(name)));
|
|
1606
|
+
VALUE node, cache, key;
|
|
1607
|
+
|
|
1608
|
+
(void)self;
|
|
1609
|
+
resolve_binding_classes();
|
|
1610
|
+
if (!ptr)
|
|
1611
|
+
return Qnil; /* caller raises with the error channel */
|
|
1612
|
+
node = rb_obj_alloc(c_b_element);
|
|
1613
|
+
rb_iv_set(node, "@c_address",
|
|
1614
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr));
|
|
1615
|
+
rb_iv_set(node, "@document", document);
|
|
1616
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
1617
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1618
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1619
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1620
|
+
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
1621
|
+
cache = binding_cache_of(document);
|
|
1622
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
1623
|
+
rb_hash_aset(cache, key, node);
|
|
1624
|
+
return node;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
static VALUE nf_create_binding_text(VALUE self, VALUE document,
|
|
1628
|
+
VALUE content)
|
|
1629
|
+
{
|
|
1630
|
+
void *doc = doc_ptr_of(document);
|
|
1631
|
+
void *ptr = f_text_create(doc, RSTRING_PTR(StringValue(content)));
|
|
1632
|
+
VALUE node, cache, key;
|
|
1633
|
+
|
|
1634
|
+
(void)self;
|
|
1635
|
+
resolve_binding_classes();
|
|
1636
|
+
if (!ptr)
|
|
1637
|
+
return Qnil;
|
|
1638
|
+
node = rb_obj_alloc(c_b_text);
|
|
1639
|
+
rb_iv_set(node, "@c_address",
|
|
1640
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr));
|
|
1641
|
+
rb_iv_set(node, "@document", document);
|
|
1642
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
1643
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1644
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1645
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1646
|
+
rb_iv_set(node, "@node_type", INT2FIX(1));
|
|
1647
|
+
cache = binding_cache_of(document);
|
|
1648
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
1649
|
+
rb_hash_aset(cache, key, node);
|
|
1650
|
+
return node;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/* ---- Bulk attribute FACES (TODO.perf/10): {name => Attr} with
|
|
1654
|
+
* the values hash beside it — the exact shapes Element#attributes
|
|
1655
|
+
* memoizes, built in one walk. Attr is a value object
|
|
1656
|
+
* (name/value/element ivars; c_handle optional). */
|
|
1657
|
+
|
|
1658
|
+
static ID id_iv_name, id_iv_value, id_iv_element;
|
|
1659
|
+
|
|
1660
|
+
static VALUE nf_bulk_attr_faces(VALUE self, VALUE addr, VALUE element)
|
|
1661
|
+
{
|
|
1662
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
1663
|
+
VALUE faces = rb_hash_new();
|
|
1664
|
+
VALUE values = rb_hash_new();
|
|
1665
|
+
void *attr = f_attr_first(node);
|
|
1666
|
+
|
|
1667
|
+
(void)self;
|
|
1668
|
+
resolve_binding_classes();
|
|
1669
|
+
while (attr) {
|
|
1670
|
+
const char *name = f_attr_name(attr);
|
|
1671
|
+
const char *value = f_attr_value(node, attr);
|
|
1672
|
+
VALUE k = name ? rb_utf8_str_new_cstr(name) : Qnil;
|
|
1673
|
+
VALUE v = value ? rb_utf8_str_new_cstr(value) : Qnil;
|
|
1674
|
+
VALUE a = rb_obj_alloc(c_b_attr);
|
|
1675
|
+
rb_iv_set(a, "@name", k);
|
|
1676
|
+
rb_iv_set(a, "@value", v);
|
|
1677
|
+
rb_iv_set(a, "@element", element);
|
|
1678
|
+
rb_hash_aset(faces, k, a);
|
|
1679
|
+
rb_hash_aset(values, k, v);
|
|
1680
|
+
attr = f_attr_next(attr);
|
|
1681
|
+
}
|
|
1682
|
+
return rb_assoc_new(faces, values);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/* ---- Ext-bound serialization (TODO.perf/04) ---------------------
|
|
1686
|
+
* The whole sized-buffer cycle in C: build SerializeOptions on
|
|
1687
|
+
* the stack, call (buf, cap, NULL status, opts), grow if needed,
|
|
1688
|
+
* return a UTF-8 String. No FFI marshaling, no Ruby buffer
|
|
1689
|
+
* management per to_xml call. */
|
|
1690
|
+
struct serialize_opts {
|
|
1691
|
+
int indent;
|
|
1692
|
+
int xml_declaration;
|
|
1693
|
+
const char *encoding;
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
/* Grow-only scratch for large serializations. Safe under the GVL:
|
|
1697
|
+
* synchronous C calls hold it, so no two Ruby threads run this
|
|
1698
|
+
* concurrently. */
|
|
1699
|
+
static char *ser_buf;
|
|
1700
|
+
static size_t ser_cap;
|
|
1701
|
+
|
|
1702
|
+
static VALUE fast_serialize(serialize_into_fn fn, void *node,
|
|
1703
|
+
int indent, int xml_declaration,
|
|
1704
|
+
VALUE encoding)
|
|
1705
|
+
{
|
|
1706
|
+
struct serialize_opts opts;
|
|
1707
|
+
char stack_buf[4096];
|
|
1708
|
+
size_t needed;
|
|
1709
|
+
|
|
1710
|
+
char *probe;
|
|
1711
|
+
size_t probe_cap;
|
|
1712
|
+
|
|
1713
|
+
opts.indent = indent;
|
|
1714
|
+
opts.xml_declaration = xml_declaration;
|
|
1715
|
+
/* The Ruby string's bytes stay valid across the synchronous
|
|
1716
|
+
* engine call (TODO.perf/36). */
|
|
1717
|
+
opts.encoding = NIL_P(encoding)
|
|
1718
|
+
? NULL
|
|
1719
|
+
: StringValueCStr(encoding);
|
|
1720
|
+
/* Probe the largest buffer we own: a big scratch from a prior
|
|
1721
|
+
* call usually fits (single serialization); cold calls start
|
|
1722
|
+
* on the stack buffer. */
|
|
1723
|
+
if (ser_cap > sizeof(stack_buf)) {
|
|
1724
|
+
probe = ser_buf;
|
|
1725
|
+
probe_cap = ser_cap;
|
|
1726
|
+
} else {
|
|
1727
|
+
probe = stack_buf;
|
|
1728
|
+
probe_cap = sizeof(stack_buf);
|
|
1729
|
+
}
|
|
1730
|
+
needed = fn(node, probe, probe_cap, NULL, &opts);
|
|
1731
|
+
if (needed == 0)
|
|
1732
|
+
return rb_utf8_str_new_cstr("");
|
|
1733
|
+
if (needed <= probe_cap)
|
|
1734
|
+
return rb_utf8_str_new(probe, needed - 1);
|
|
1735
|
+
if (needed > ser_cap) {
|
|
1736
|
+
ser_buf = ruby_xrealloc(ser_buf, needed);
|
|
1737
|
+
ser_cap = needed;
|
|
1738
|
+
}
|
|
1739
|
+
needed = fn(node, ser_buf, ser_cap, NULL, &opts);
|
|
1740
|
+
return needed > 0 ? rb_utf8_str_new(ser_buf, needed - 1)
|
|
1741
|
+
: rb_utf8_str_new_cstr("");
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
static VALUE nf_fast_document_xml(VALUE self, VALUE addr,
|
|
1745
|
+
VALUE indent, VALUE decl,
|
|
1746
|
+
VALUE encoding)
|
|
1747
|
+
{
|
|
1748
|
+
(void)self;
|
|
1749
|
+
return fast_serialize(f_doc_serialize,
|
|
1750
|
+
(void *)(uintptr_t)NUM2ULL(addr),
|
|
1751
|
+
NUM2INT(indent), RTEST(decl) ? 1 : 0,
|
|
1752
|
+
encoding);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
|
|
1756
|
+
VALUE indent, VALUE decl,
|
|
1757
|
+
VALUE encoding)
|
|
1758
|
+
{
|
|
1759
|
+
(void)self;
|
|
1760
|
+
return fast_serialize(f_elem_serialize,
|
|
1761
|
+
(void *)(uintptr_t)NUM2ULL(addr),
|
|
1762
|
+
NUM2INT(indent), RTEST(decl) ? 1 : 0,
|
|
1763
|
+
encoding);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/* ---- Bulk hydration walker (TODO.perf/38, #230) -----------------
|
|
1767
|
+
* Pre-order walk that materializes one Hash per element node
|
|
1768
|
+
* (kind, name, prefix, uri, attrs as Array<[name,value]>, text,
|
|
1769
|
+
* depth, line) and yields it to a Ruby Proc (walk_subtree) or
|
|
1770
|
+
* collects it into an Array (snapshot). No per-node Node wrapper
|
|
1771
|
+
* construction — the consumer iterates rows and builds their
|
|
1772
|
+
* typed objects directly. */
|
|
1773
|
+
/* Node kinds (mirror types.h). Documented in descriptor comment. */
|
|
1774
|
+
#define WS_NODE_ELEMENT 0
|
|
1775
|
+
#define WS_NODE_TEXT 1
|
|
1776
|
+
#define WS_NODE_COMMENT 2
|
|
1777
|
+
#define WS_NODE_CDATA 3
|
|
1778
|
+
#define WS_NODE_PI 4
|
|
1779
|
+
#define WS_NODE_DOCTYPE 5
|
|
1780
|
+
|
|
1781
|
+
static VALUE build_element_row(VALUE doc, void *elem, int depth)
|
|
1782
|
+
{
|
|
1783
|
+
VALUE row = rb_hash_new();
|
|
1784
|
+
rb_hash_aset(row, ID2SYM(rb_intern("kind")),
|
|
1785
|
+
rb_utf8_str_new_cstr("element"));
|
|
1786
|
+
const char *name = f_elem_name(elem);
|
|
1787
|
+
rb_hash_aset(row, ID2SYM(rb_intern("name")),
|
|
1788
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil);
|
|
1789
|
+
rb_hash_aset(row, ID2SYM(rb_intern("prefix")),
|
|
1790
|
+
f_elem_prefix(elem) ? rb_utf8_str_new_cstr(f_elem_prefix(elem)) : Qnil);
|
|
1791
|
+
rb_hash_aset(row, ID2SYM(rb_intern("uri")),
|
|
1792
|
+
f_elem_ns(elem) ? rb_utf8_str_new_cstr(f_elem_ns(elem)) : Qnil);
|
|
1793
|
+
/* attrs: name/value pairs as Array<[name, value]>. */
|
|
1794
|
+
VALUE attrs = rb_ary_new();
|
|
1795
|
+
void *a = f_attr_first(elem);
|
|
1796
|
+
while (a) {
|
|
1797
|
+
VALUE pair = rb_ary_new_capa(2);
|
|
1798
|
+
const char *an = f_attr_name(a);
|
|
1799
|
+
const char *av = f_attr_value(elem, a);
|
|
1800
|
+
rb_ary_push(pair, an ? rb_utf8_str_new_cstr(an) : Qnil);
|
|
1801
|
+
rb_ary_push(pair, av ? rb_utf8_str_new_cstr(av) : Qnil);
|
|
1802
|
+
rb_ary_push(attrs, pair);
|
|
1803
|
+
a = f_attr_next(a);
|
|
1804
|
+
}
|
|
1805
|
+
rb_hash_aset(row, ID2SYM(rb_intern("attrs")), attrs);
|
|
1806
|
+
/* text: first text-child content (moxml-style common case). */
|
|
1807
|
+
void *c = f_first_child(elem);
|
|
1808
|
+
VALUE text = Qnil;
|
|
1809
|
+
while (c) {
|
|
1810
|
+
if (f_node_type(c) == WS_NODE_TEXT) {
|
|
1811
|
+
const char *t = f_text_content(c);
|
|
1812
|
+
text = t ? rb_utf8_str_new_cstr(t) : Qnil;
|
|
1813
|
+
break;
|
|
1814
|
+
}
|
|
1815
|
+
c = f_next_sibling(c);
|
|
1816
|
+
}
|
|
1817
|
+
rb_hash_aset(row, ID2SYM(rb_intern("text")), text);
|
|
1818
|
+
rb_hash_aset(row, ID2SYM(rb_intern("depth")), INT2NUM(depth));
|
|
1819
|
+
return row;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
static VALUE build_text_row(int kind, int depth, const char *t)
|
|
1823
|
+
{
|
|
1824
|
+
VALUE row = rb_hash_new();
|
|
1825
|
+
rb_hash_aset(row, ID2SYM(rb_intern("kind")),
|
|
1826
|
+
rb_utf8_str_new_cstr(kind == WS_NODE_TEXT ? "text"
|
|
1827
|
+
: kind == WS_NODE_COMMENT ? "comment"
|
|
1828
|
+
: kind == WS_NODE_CDATA ? "cdata" : "other"));
|
|
1829
|
+
rb_hash_aset(row, ID2SYM(rb_intern("text")),
|
|
1830
|
+
t ? rb_utf8_str_new_cstr(t) : Qnil);
|
|
1831
|
+
rb_hash_aset(row, ID2SYM(rb_intern("depth")), INT2NUM(depth));
|
|
1832
|
+
return row;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
static void yield_or_collect_row(VALUE out_or_block, VALUE row, int collect)
|
|
1836
|
+
{
|
|
1837
|
+
if (collect) {
|
|
1838
|
+
rb_ary_push(out_or_block, row);
|
|
1839
|
+
} else {
|
|
1840
|
+
rb_funcall(out_or_block, rb_intern("call"), 1, row);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
static void walk_subtree_impl(VALUE doc, void *node, int depth, VALUE out_or_block, int collect)
|
|
1845
|
+
{
|
|
1846
|
+
int kind = f_node_type(node);
|
|
1847
|
+
if (kind == WS_NODE_ELEMENT) {
|
|
1848
|
+
void *c;
|
|
1849
|
+
yield_or_collect_row(out_or_block, build_element_row(doc, node, depth), collect);
|
|
1850
|
+
c = f_first_child(node);
|
|
1851
|
+
while (c) {
|
|
1852
|
+
walk_subtree_impl(doc, c, depth + 1, out_or_block, collect);
|
|
1853
|
+
c = f_next_sibling(c);
|
|
1854
|
+
}
|
|
1855
|
+
} else if (kind == WS_NODE_DOCTYPE || kind == 9 /* DOCUMENT */) {
|
|
1856
|
+
void *dc = f_first_child(node);
|
|
1857
|
+
while (dc) {
|
|
1858
|
+
walk_subtree_impl(doc, dc, depth, out_or_block, collect);
|
|
1859
|
+
dc = f_next_sibling(dc);
|
|
1860
|
+
}
|
|
1861
|
+
} else if (kind == WS_NODE_TEXT || kind == WS_NODE_COMMENT ||
|
|
1862
|
+
kind == WS_NODE_CDATA || kind == WS_NODE_PI) {
|
|
1863
|
+
const char *t = NULL;
|
|
1864
|
+
if (kind == WS_NODE_TEXT)
|
|
1865
|
+
t = f_text_content(node);
|
|
1866
|
+
else if (kind == WS_NODE_COMMENT)
|
|
1867
|
+
t = f_comment_content(node);
|
|
1868
|
+
else if (kind == WS_NODE_CDATA)
|
|
1869
|
+
t = f_cdata_content(node);
|
|
1870
|
+
else if (kind == WS_NODE_PI)
|
|
1871
|
+
t = f_pi_data(node);
|
|
1872
|
+
yield_or_collect_row(out_or_block, build_text_row(kind, depth, t), collect);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
static VALUE nf_snapshot_subtree(VALUE self, VALUE document, VALUE addr)
|
|
1877
|
+
{
|
|
1878
|
+
VALUE out = rb_ary_new();
|
|
1879
|
+
walk_subtree_impl(document, (void *)(uintptr_t)NUM2ULL(addr), 0, out, 1);
|
|
1880
|
+
return out;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/* ---- Lean plan rows (lutaml/moxml#249 plan layer) ----------------
|
|
1884
|
+
* Elements only, one 4-tuple per element: [name, attrs_flat, text,
|
|
1885
|
+
* depth] — no Hash per row, no text/comment rows, attr and element
|
|
1886
|
+
* NAMES interned (shared frozen fstrings: names repeat across the
|
|
1887
|
+
* document, so repeat reads allocate zero). Pre-order, depth 0 at
|
|
1888
|
+
* the addressed node. */
|
|
1889
|
+
static void walk_rows_impl(void *node, int depth, VALUE out)
|
|
1890
|
+
{
|
|
1891
|
+
if (f_node_type(node) == WS_NODE_ELEMENT) {
|
|
1892
|
+
VALUE row = rb_ary_new_capa(4);
|
|
1893
|
+
VALUE attrs;
|
|
1894
|
+
const char *name = f_elem_name(node);
|
|
1895
|
+
const char *an;
|
|
1896
|
+
const char *av;
|
|
1897
|
+
void *a;
|
|
1898
|
+
void *c;
|
|
1899
|
+
|
|
1900
|
+
rb_ary_store(row, 0, name ? rb_enc_interned_str(name, strlen(name),
|
|
1901
|
+
rb_utf8_encoding())
|
|
1902
|
+
: Qnil);
|
|
1903
|
+
attrs = rb_ary_new();
|
|
1904
|
+
for (a = f_attr_first(node); a; a = f_attr_next(a)) {
|
|
1905
|
+
an = f_attr_name(a);
|
|
1906
|
+
av = f_attr_value(node, a);
|
|
1907
|
+
rb_ary_push(attrs, an ? rb_enc_interned_str(an, strlen(an),
|
|
1908
|
+
rb_utf8_encoding())
|
|
1909
|
+
: Qnil);
|
|
1910
|
+
rb_ary_push(attrs, av ? rb_utf8_str_new_cstr(av) : Qnil);
|
|
1911
|
+
}
|
|
1912
|
+
rb_ary_store(row, 1, attrs);
|
|
1913
|
+
for (c = f_first_child(node); c; c = f_next_sibling(c)) {
|
|
1914
|
+
if (f_node_type(c) == WS_NODE_TEXT) {
|
|
1915
|
+
const char *t = f_text_content(c);
|
|
1916
|
+
rb_ary_store(row, 2, t ? rb_utf8_str_new_cstr(t) : Qnil);
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
rb_ary_store(row, 3, INT2NUM(depth));
|
|
1921
|
+
rb_ary_push(out, row);
|
|
1922
|
+
|
|
1923
|
+
for (c = f_first_child(node); c; c = f_next_sibling(c)) {
|
|
1924
|
+
walk_rows_impl(c, depth + 1, out);
|
|
1925
|
+
}
|
|
1926
|
+
} else if (f_node_type(node) == WS_NODE_DOCTYPE || f_node_type(node) == 9) {
|
|
1927
|
+
void *dc = f_first_child(node);
|
|
1928
|
+
while (dc) {
|
|
1929
|
+
walk_rows_impl(dc, depth, out);
|
|
1930
|
+
dc = f_next_sibling(dc);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
static VALUE nf_snapshot_rows(VALUE self, VALUE document, VALUE addr)
|
|
1936
|
+
{
|
|
1937
|
+
(void)self;
|
|
1938
|
+
(void)document;
|
|
1939
|
+
VALUE out = rb_ary_new();
|
|
1940
|
+
walk_rows_impl((void *)(uintptr_t)NUM2ULL(addr), 0, out);
|
|
1941
|
+
return out;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/* ---- inner_html in one C pass (TODO.perf/18) --------------------
|
|
1945
|
+
* Serializes the receiver's children into one growable buffer:
|
|
1946
|
+
* elements via leptris_element_serialize_into (the same opts the
|
|
1947
|
+
* Ruby loop passed), text XML-escaped with the binding's entity
|
|
1948
|
+
* set (& < > \r), CDATA/comments/PIs wrapped raw — byte-identical
|
|
1949
|
+
* to Element#inner_html's per-child loop. */
|
|
1950
|
+
static char *inner_buf;
|
|
1951
|
+
static size_t inner_cap;
|
|
1952
|
+
|
|
1953
|
+
static void inner_ensure(size_t need)
|
|
1954
|
+
{
|
|
1955
|
+
if (need > inner_cap) {
|
|
1956
|
+
inner_cap = need < 64 ? 64 : need;
|
|
1957
|
+
inner_buf = ruby_xrealloc(inner_buf, inner_cap);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
static size_t inner_append_raw(const char *s, size_t len, size_t at)
|
|
1962
|
+
{
|
|
1963
|
+
inner_ensure(at + len + 1);
|
|
1964
|
+
if (len)
|
|
1965
|
+
memcpy(inner_buf + at, s, len);
|
|
1966
|
+
return at + len;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
static size_t inner_append_escaped(const char *s, size_t at)
|
|
1970
|
+
{
|
|
1971
|
+
for (; s && *s; s++) {
|
|
1972
|
+
const char *repl;
|
|
1973
|
+
size_t rl;
|
|
1974
|
+
switch (*s) {
|
|
1975
|
+
case '&': repl = "&"; rl = 5; break;
|
|
1976
|
+
case '<': repl = "<"; rl = 4; break;
|
|
1977
|
+
case '>': repl = ">"; rl = 4; break;
|
|
1978
|
+
case '\r': repl = "
"; rl = 5; break;
|
|
1979
|
+
default: repl = NULL; rl = 0; break;
|
|
1980
|
+
}
|
|
1981
|
+
if (repl) {
|
|
1982
|
+
at = inner_append_raw(repl, rl, at);
|
|
1983
|
+
} else {
|
|
1984
|
+
inner_ensure(at + 2);
|
|
1985
|
+
inner_buf[at++] = *s;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return at;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
static VALUE nf_fast_inner_xml(VALUE self, VALUE addr)
|
|
1992
|
+
{
|
|
1993
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
1994
|
+
void *child;
|
|
1995
|
+
struct serialize_opts opts;
|
|
1996
|
+
size_t at = 0, needed;
|
|
1997
|
+
|
|
1998
|
+
(void)self;
|
|
1999
|
+
opts.indent = 0;
|
|
2000
|
+
opts.xml_declaration = 1; /* matches element_xml_default */
|
|
2001
|
+
opts.encoding = NULL;
|
|
2002
|
+
inner_ensure(4096);
|
|
2003
|
+
for (child = f_first_child(node); child != NULL;
|
|
2004
|
+
child = f_next_sibling(child)) {
|
|
2005
|
+
switch (f_node_type(child)) {
|
|
2006
|
+
case 0: /* element */
|
|
2007
|
+
needed = f_elem_serialize(child, inner_buf + at,
|
|
2008
|
+
inner_cap - at, NULL, &opts);
|
|
2009
|
+
if (needed > 0) {
|
|
2010
|
+
if (needed > inner_cap - at) {
|
|
2011
|
+
inner_ensure(at + needed);
|
|
2012
|
+
needed = f_elem_serialize(child, inner_buf + at,
|
|
2013
|
+
inner_cap - at, NULL, &opts);
|
|
2014
|
+
}
|
|
2015
|
+
at += needed - 1; /* size includes the NUL */
|
|
2016
|
+
}
|
|
2017
|
+
break;
|
|
2018
|
+
case 1: /* text */
|
|
2019
|
+
at = inner_append_escaped(f_text_content(child), at);
|
|
2020
|
+
break;
|
|
2021
|
+
case 2: /* comment */
|
|
2022
|
+
at = inner_append_raw("<!--", 4, at);
|
|
2023
|
+
at = inner_append_escaped(f_comment_content(child), at);
|
|
2024
|
+
at = inner_append_raw("-->", 3, at);
|
|
2025
|
+
break;
|
|
2026
|
+
case 3: { /* CDATA */
|
|
2027
|
+
const char *c = f_cdata_content(child);
|
|
2028
|
+
at = inner_append_raw("<![CDATA[", 9, at);
|
|
2029
|
+
at = inner_append_raw(c, c ? strlen(c) : 0, at);
|
|
2030
|
+
at = inner_append_raw("]]>", 3, at);
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
case 4: { /* PI — the binding strips leading whitespace from
|
|
2034
|
+
* the data before joining (read_pi_data parity). */
|
|
2035
|
+
const char *data = f_pi_data(child);
|
|
2036
|
+
while (data && (*data == ' ' || *data == '\t' ||
|
|
2037
|
+
*data == '\r' || *data == '\n'))
|
|
2038
|
+
data++;
|
|
2039
|
+
at = inner_append_raw("<?", 2, at);
|
|
2040
|
+
at = inner_append_escaped(f_pi_target(child), at);
|
|
2041
|
+
if (data && *data) {
|
|
2042
|
+
at = inner_append_raw(" ", 1, at);
|
|
2043
|
+
at = inner_append_escaped(data, at);
|
|
2044
|
+
}
|
|
2045
|
+
at = inner_append_raw("?>", 2, at);
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
default:
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return rb_utf8_str_new(inner_buf, at);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/* ---- Adoption-lift predicate (TODO.perf/09, #204 ask 3) ---------
|
|
2056
|
+
* The #178/#208 namespace lift + pruning runs on EVERY attach and
|
|
2057
|
+
* costs several FFI round-trips even when provably a no-op. This
|
|
2058
|
+
* predicate answers in ONE dispatch: false when the node carries
|
|
2059
|
+
* no namespace declarations AND its name has no prefix — the
|
|
2060
|
+
* common programmatic-build shape (bare names, no namespaces). */
|
|
2061
|
+
/* ---- Document lifetime in C (TODO.perf/12) ----------------------
|
|
2062
|
+
* A TypedData handle holding the C document pointer, referenced
|
|
2063
|
+
* only by the binding Document's @doc_handle ivar — its lifetime
|
|
2064
|
+
* IS the document's. dfree releases the C document directly: no
|
|
2065
|
+
* Ruby finalizer, no FFI dispatch from finalizer context.
|
|
2066
|
+
* Document#free releases the pointer first, so dfree no-ops —
|
|
2067
|
+
* the same double-free protocol the Freed struct enforces on the
|
|
2068
|
+
* Ruby-finalizer path (which stays for LEPTRIS_NO_NATIVE). */
|
|
2069
|
+
struct doc_handle {
|
|
2070
|
+
void *doc;
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
static void dh_free(void *p)
|
|
2074
|
+
{
|
|
2075
|
+
struct doc_handle *h = p;
|
|
2076
|
+
if (h->doc) {
|
|
2077
|
+
f_doc_free(h->doc);
|
|
2078
|
+
h->doc = NULL;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
static size_t dh_size(const void *p)
|
|
2083
|
+
{
|
|
2084
|
+
(void)p;
|
|
2085
|
+
return sizeof(struct doc_handle);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
static const rb_data_type_t dh_type = {
|
|
2089
|
+
"Leptris/XML/DocHandle",
|
|
2090
|
+
{ 0, dh_free, dh_size, },
|
|
2091
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
2092
|
+
};
|
|
2093
|
+
|
|
2094
|
+
static VALUE c_doc_handle;
|
|
2095
|
+
static ID id_iv_doc_handle, id_freed_new, id_alive;
|
|
2096
|
+
|
|
2097
|
+
/* Node#dup in one dispatch (TODO.perf/30): engine create +
|
|
2098
|
+
* lifetime handle + document wrapper + element_copy + the copy
|
|
2099
|
+
* wrapped-and-rooted (memo-seeded). The namespace lift stays a
|
|
2100
|
+
* Ruby decision (skip_adoption_lift? — the copy_of seam's
|
|
2101
|
+
* semantics, #696/#721/#812). */
|
|
2102
|
+
static VALUE nf_copy_binding_element(VALUE self, VALUE document,
|
|
2103
|
+
VALUE addr)
|
|
2104
|
+
{
|
|
2105
|
+
void *doc, *copy;
|
|
2106
|
+
VALUE new_doc, doc_addr, handle, freed, root;
|
|
2107
|
+
struct doc_handle *h;
|
|
2108
|
+
|
|
2109
|
+
(void)self;
|
|
2110
|
+
resolve_binding_classes();
|
|
2111
|
+
doc = f_doc_create();
|
|
2112
|
+
if (!doc)
|
|
2113
|
+
return Qnil;
|
|
2114
|
+
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
2115
|
+
new_doc = rb_obj_alloc(c_b_document);
|
|
2116
|
+
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
2117
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
2118
|
+
rb_iv_set(new_doc, "@freed", freed);
|
|
2119
|
+
rb_iv_set(new_doc, "@readonly", Qfalse);
|
|
2120
|
+
rb_iv_set(new_doc, "@version", INT2FIX(0));
|
|
2121
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
2122
|
+
&dh_type, h);
|
|
2123
|
+
h->doc = doc;
|
|
2124
|
+
rb_ivar_set(new_doc, id_iv_doc_handle, handle);
|
|
2125
|
+
|
|
2126
|
+
copy = f_elem_copy((void *)(uintptr_t)NUM2ULL(addr), doc);
|
|
2127
|
+
if (!copy) {
|
|
2128
|
+
h->doc = NULL; /* the handle releases the empty doc */
|
|
2129
|
+
return Qnil;
|
|
2130
|
+
}
|
|
2131
|
+
root = wrap_addr_node(new_doc, copy);
|
|
2132
|
+
rb_ivar_set(new_doc, rb_intern("@root"), root);
|
|
2133
|
+
rb_ivar_set(new_doc, rb_intern("@root_version"), INT2FIX(0));
|
|
2134
|
+
return new_doc;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/* Shared document-wrapper construction from a C document
|
|
2138
|
+
* pointer: ivar-seeded wrapper + lifetime handle. Returns the
|
|
2139
|
+
* wrapper. (TODO.perf/35 — the create face inlines the same
|
|
2140
|
+
* steps.) */
|
|
2141
|
+
static VALUE build_binding_document(void *doc)
|
|
2142
|
+
{
|
|
2143
|
+
VALUE new_doc, doc_addr, handle, freed;
|
|
2144
|
+
struct doc_handle *h;
|
|
2145
|
+
|
|
2146
|
+
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
2147
|
+
new_doc = rb_obj_alloc(c_b_document);
|
|
2148
|
+
rb_iv_set(new_doc, "@c_ptr",
|
|
2149
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
|
|
2150
|
+
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
2151
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
2152
|
+
rb_iv_set(new_doc, "@freed", freed);
|
|
2153
|
+
rb_iv_set(new_doc, "@readonly", Qfalse);
|
|
2154
|
+
rb_iv_set(new_doc, "@version", INT2FIX(0));
|
|
2155
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
2156
|
+
&dh_type, h);
|
|
2157
|
+
h->doc = doc;
|
|
2158
|
+
rb_ivar_set(new_doc, id_iv_doc_handle, handle);
|
|
2159
|
+
return new_doc;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/* check_status in C (TODO.perf/36): the exact status_message
|
|
2163
|
+
* format — "base" or "base (detail)" — so the mutation faces
|
|
2164
|
+
* raise instead of returning codes for a Ruby dispatch. */
|
|
2165
|
+
static void check_status_c(int st)
|
|
2166
|
+
{
|
|
2167
|
+
const char *base, *detail;
|
|
2168
|
+
|
|
2169
|
+
if (st == 0)
|
|
2170
|
+
return;
|
|
2171
|
+
base = f_status_str(st);
|
|
2172
|
+
detail = f_last_err();
|
|
2173
|
+
if (detail && *detail)
|
|
2174
|
+
rb_raise(c_leptris_error, "%s (%s)",
|
|
2175
|
+
base ? base : "?", detail);
|
|
2176
|
+
rb_raise(c_leptris_error, "%s", base ? base : "?");
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
/* Document.parse default path in one dispatch (TODO.perf/35):
|
|
2180
|
+
* leptris_parse_string + wrapper + handle. Qnil = parse failure
|
|
2181
|
+
* (the caller raises with the same message shape). */
|
|
2182
|
+
static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
|
|
2183
|
+
{
|
|
2184
|
+
void *doc;
|
|
2185
|
+
|
|
2186
|
+
(void)self;
|
|
2187
|
+
resolve_binding_classes();
|
|
2188
|
+
doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
|
|
2189
|
+
(size_t)RSTRING_LEN(StringValue(xml)), NULL);
|
|
2190
|
+
return doc ? build_binding_document(doc) : Qnil;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/* Fragment lane (TODO.perf/34): parse the markup against the
|
|
2194
|
+
* document; returns the fragment's ADDRESS, or Qfalse when the
|
|
2195
|
+
* parse fails (status is not surfaced — the caller re-runs the
|
|
2196
|
+
* FFI path for the exact error). */
|
|
2197
|
+
static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
|
|
2198
|
+
VALUE xml)
|
|
2199
|
+
{
|
|
2200
|
+
void *frag;
|
|
2201
|
+
int status;
|
|
2202
|
+
|
|
2203
|
+
(void)self;
|
|
2204
|
+
resolve_binding_classes();
|
|
2205
|
+
frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
|
|
2206
|
+
(size_t)RSTRING_LEN(StringValue(xml)),
|
|
2207
|
+
doc_ptr_of(document), &status);
|
|
2208
|
+
return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/* One-shot markup append (TODO.perf/34): parse the markup, walk
|
|
2212
|
+
* the fragment's children, append each to the parent — one
|
|
2213
|
+
* readonly gate + version bump for the whole batch. Returns the
|
|
2214
|
+
* appended count; -1 = parse failure; status codes surface as
|
|
2215
|
+
* negatives (-1000 - st) for the caller's check_status path. */
|
|
2216
|
+
static VALUE nf_append_markup(VALUE self, VALUE document,
|
|
2217
|
+
VALUE parent_addr, VALUE xml)
|
|
2218
|
+
{
|
|
2219
|
+
void *frag, *parent, *child, *next_child;
|
|
2220
|
+
int status, count = 0;
|
|
2221
|
+
|
|
2222
|
+
(void)self;
|
|
2223
|
+
resolve_binding_classes();
|
|
2224
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
2225
|
+
rb_raise(c_use_after_free_error,
|
|
2226
|
+
"owning document has been freed");
|
|
2227
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
2228
|
+
rb_raise(c_readonly_error,
|
|
2229
|
+
"document is readonly — mutation attempted");
|
|
2230
|
+
frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
|
|
2231
|
+
(size_t)RSTRING_LEN(StringValue(xml)),
|
|
2232
|
+
doc_ptr_of(document), &status);
|
|
2233
|
+
if (!frag)
|
|
2234
|
+
return INT2FIX(-1);
|
|
2235
|
+
parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
2236
|
+
rb_ivar_set(document, id_iv_version,
|
|
2237
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
2238
|
+
/* Appending DETACHES the child from the fragment — capture
|
|
2239
|
+
* the next sibling before each move or the walk corrupts. */
|
|
2240
|
+
for (child = f_first_child(frag); child != NULL; child = next_child) {
|
|
2241
|
+
next_child = f_next_sibling(child);
|
|
2242
|
+
status = f_append_child(parent, child);
|
|
2243
|
+
if (status != 0)
|
|
2244
|
+
check_status_c(status);
|
|
2245
|
+
count++;
|
|
2246
|
+
}
|
|
2247
|
+
return INT2FIX(count);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
|
|
2251
|
+
{
|
|
2252
|
+
struct doc_handle *h;
|
|
2253
|
+
VALUE handle;
|
|
2254
|
+
|
|
2255
|
+
(void)self;
|
|
2256
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
2257
|
+
&dh_type, h);
|
|
2258
|
+
h->doc = doc_ptr_of(document);
|
|
2259
|
+
rb_ivar_set(document, id_iv_doc_handle, handle);
|
|
2260
|
+
return handle;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/* Document#free already released the C memory through the FFI
|
|
2264
|
+
* seam; detach so the GC-pass dfree no-ops. */
|
|
2265
|
+
static VALUE nf_doc_handle_release(VALUE self, VALUE document)
|
|
2266
|
+
{
|
|
2267
|
+
VALUE handle = rb_ivar_get(document, id_iv_doc_handle);
|
|
2268
|
+
(void)self;
|
|
2269
|
+
if (handle != Qnil) {
|
|
2270
|
+
struct doc_handle *h;
|
|
2271
|
+
TypedData_Get_Struct(handle, struct doc_handle, &dh_type, h);
|
|
2272
|
+
h->doc = NULL;
|
|
2273
|
+
}
|
|
2274
|
+
return Qnil;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/* The full Document.create in one dispatch: engine create,
|
|
2278
|
+
* binding wrapper (ivar-seeded, bypassing initialize), and the
|
|
2279
|
+
* lifetime handle. Returns Qnil when the engine refuses. */
|
|
2280
|
+
static VALUE nf_create_binding_document(VALUE self)
|
|
2281
|
+
{
|
|
2282
|
+
void *doc;
|
|
2283
|
+
VALUE document, addr, handle, freed;
|
|
2284
|
+
struct doc_handle *h;
|
|
2285
|
+
|
|
2286
|
+
(void)self;
|
|
2287
|
+
resolve_binding_classes();
|
|
2288
|
+
doc = f_doc_create();
|
|
2289
|
+
if (!doc)
|
|
2290
|
+
return Qnil;
|
|
2291
|
+
addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
2292
|
+
document = rb_obj_alloc(c_b_document);
|
|
2293
|
+
rb_iv_set(document, "@c_ptr",
|
|
2294
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, addr));
|
|
2295
|
+
rb_iv_set(document, "@c_address", addr);
|
|
2296
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
2297
|
+
rb_iv_set(document, "@freed", freed);
|
|
2298
|
+
rb_iv_set(document, "@readonly", Qfalse);
|
|
2299
|
+
rb_iv_set(document, "@version", INT2FIX(0));
|
|
2300
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
2301
|
+
&dh_type, h);
|
|
2302
|
+
h->doc = doc;
|
|
2303
|
+
rb_ivar_set(document, id_iv_doc_handle, handle);
|
|
2304
|
+
return document;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/* ---- Address-based fast readers (TODO.perf/01) -----------------
|
|
2308
|
+
* The DEFAULT binding classes call these when the bundle is
|
|
2309
|
+
* loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
|
|
2310
|
+
* marshaling. Addresses come from FFI::Pointer#address (a Ruby
|
|
2311
|
+
* ivar read on the ffi gem's Pointer). */
|
|
2312
|
+
static VALUE nf_name(VALUE self, VALUE addr)
|
|
2313
|
+
{
|
|
2314
|
+
const char *s;
|
|
2315
|
+
(void)self;
|
|
2316
|
+
s = f_elem_name((void *)(uintptr_t)NUM2ULL(addr));
|
|
2317
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
static VALUE nf_element_text(VALUE self, VALUE addr)
|
|
2321
|
+
{
|
|
2322
|
+
const char *s;
|
|
2323
|
+
(void)self;
|
|
2324
|
+
s = f_element_text((void *)(uintptr_t)NUM2ULL(addr));
|
|
2325
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
static VALUE nf_attribute(VALUE self, VALUE addr, VALUE name)
|
|
2329
|
+
{
|
|
2330
|
+
const char *s;
|
|
2331
|
+
(void)self;
|
|
2332
|
+
s = f_attr((void *)(uintptr_t)NUM2ULL(addr), StringValueCStr(name));
|
|
2333
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
static VALUE nf_prefix(VALUE self, VALUE addr)
|
|
2337
|
+
{
|
|
2338
|
+
const char *s;
|
|
2339
|
+
(void)self;
|
|
2340
|
+
s = f_elem_prefix((void *)(uintptr_t)NUM2ULL(addr));
|
|
2341
|
+
return (s && *s) ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
static VALUE nn_from(VALUE klass, VALUE document, VALUE element)
|
|
2345
|
+
{
|
|
2346
|
+
/* Element address comes through the binding's #c_ptr address;
|
|
2347
|
+
* registered in the shared identity cache so parent/sibling
|
|
2348
|
+
* round-trips return the same object. */
|
|
2349
|
+
struct native_node *n;
|
|
2350
|
+
VALUE node = nn_allocate(klass);
|
|
2351
|
+
void *ptr = (void *)(uintptr_t)NUM2ULL(
|
|
2352
|
+
rb_funcall(element, id_address, 0));
|
|
2353
|
+
TypedData_Get_Struct(node, struct native_node, &nn_type, n);
|
|
2354
|
+
n->ptr = ptr;
|
|
2355
|
+
n->document = document;
|
|
2356
|
+
rb_hash_aset(native_cache_of(document),
|
|
2357
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr), node);
|
|
2358
|
+
return node;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/* Ruby-side bootstrap: resolve the libleptris symbols once, from
|
|
2362
|
+
* the path the FFI layer loaded. */
|
|
2363
|
+
static VALUE leptris_native_resolve_rb(VALUE self, VALUE lib_path)
|
|
2364
|
+
{
|
|
2365
|
+
(void)self;
|
|
2366
|
+
resolve_symbols(StringValueCStr(lib_path));
|
|
2367
|
+
return Qnil;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
void Init_native(void)
|
|
2371
|
+
{
|
|
2372
|
+
VALUE m_leptris, m_xml, m_native;
|
|
2373
|
+
|
|
2374
|
+
m_leptris = rb_define_module("Leptris");
|
|
2375
|
+
m_xml = rb_define_module_under(m_leptris, "XML");
|
|
2376
|
+
c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
|
|
2377
|
+
rb_undef_alloc_func(c_native_node);
|
|
2378
|
+
|
|
2379
|
+
id_iv_name = rb_intern("@name");
|
|
2380
|
+
id_iv_value = rb_intern("@value");
|
|
2381
|
+
id_iv_element = rb_intern("@element");
|
|
2382
|
+
id_iv_c_address = rb_intern("@c_address");
|
|
2383
|
+
id_iv_native_cache = rb_intern("@native_cache");
|
|
2384
|
+
id_iv_binding_cache = rb_intern("@wrapper_cache");
|
|
2385
|
+
id_iv_version = rb_intern("@version");
|
|
2386
|
+
id_iv_readonly = rb_intern("@readonly");
|
|
2387
|
+
id_address = rb_intern("address");
|
|
2388
|
+
id_iv_nn_content = rb_intern("@nn_content");
|
|
2389
|
+
id_iv_nn_content_ver = rb_intern("@nn_content_ver");
|
|
2390
|
+
id_iv_nn_attrs = rb_intern("@nn_attrs");
|
|
2391
|
+
id_iv_nn_attrs_ver = rb_intern("@nn_attrs_ver");
|
|
2392
|
+
id_child_klasses = rb_intern("@nn_child_klasses");
|
|
2393
|
+
id_iv_doc_handle = rb_intern("@doc_handle");
|
|
2394
|
+
id_freed_new = rb_intern("new");
|
|
2395
|
+
id_alive = rb_intern("alive");
|
|
2396
|
+
c_readonly_error = rb_path2class("Leptris::XML::ReadOnlyError");
|
|
2397
|
+
c_use_after_free_error =
|
|
2398
|
+
rb_path2class("Leptris::XML::UseAfterFreeError");
|
|
2399
|
+
rb_gc_register_mark_object(c_readonly_error);
|
|
2400
|
+
rb_gc_register_mark_object(c_use_after_free_error);
|
|
2401
|
+
|
|
2402
|
+
rb_define_singleton_method(c_native_node, "from", nn_from, 2);
|
|
2403
|
+
rb_define_singleton_method(c_native_node, "install_child_klasses",
|
|
2404
|
+
nn_install_child_klasses, 1);
|
|
2405
|
+
rb_define_singleton_method(c_native_node, "create_element", nn_create_element, 2);
|
|
2406
|
+
rb_define_singleton_method(c_native_node, "create_text", nn_create_text, 2);
|
|
2407
|
+
rb_define_singleton_method(c_native_node, "set_root", nn_set_root, 2);
|
|
2408
|
+
rb_define_method(c_native_node, "create_child", nn_create_child, 1);
|
|
2409
|
+
rb_define_method(c_native_node, "append_child", nn_append_child, 1);
|
|
2410
|
+
rb_define_method(c_native_node, "add_child", nn_append_child, 1);
|
|
2411
|
+
rb_define_method(c_native_node, "address", nn_address, 0);
|
|
2412
|
+
rb_define_method(c_native_node, "document", nn_document, 0);
|
|
2413
|
+
rb_define_method(c_native_node, "attributes", nn_attributes, 0);
|
|
2414
|
+
rb_define_method(c_native_node, "line", nn_line, 0);
|
|
2415
|
+
rb_define_method(c_native_node, "byte_offset", nn_byte_offset, 0);
|
|
2416
|
+
rb_define_method(c_native_node, "name", nn_name, 0);
|
|
2417
|
+
rb_define_method(c_native_node, "content", nn_content, 0);
|
|
2418
|
+
rb_define_method(c_native_node, "attribute", nn_attribute, 1);
|
|
2419
|
+
rb_define_alias(c_native_node, "[]", "attribute");
|
|
2420
|
+
/* Unshadowed aliases for consumers whose contract modules shadow
|
|
2421
|
+
* attribute/content: a plain call resolves to these at normal
|
|
2422
|
+
* method-cache cost instead of UnboundMethod#bind_call. */
|
|
2423
|
+
rb_define_alias(c_native_node, "attr_read", "attribute");
|
|
2424
|
+
rb_define_alias(c_native_node, "text_read", "content");
|
|
2425
|
+
rb_define_method(c_native_node, "children", nn_children, 0);
|
|
2426
|
+
rb_define_method(c_native_node, "element_children", nn_element_children, 0);
|
|
2427
|
+
rb_define_method(c_native_node, "next_sibling", nn_next_sibling, 0);
|
|
2428
|
+
rb_define_method(c_native_node, "parent", nn_parent, 0);
|
|
2429
|
+
rb_define_method(c_native_node, "node_type", nn_type_sym, 0);
|
|
2430
|
+
rb_define_singleton_method(c_native_node, "resolve!",
|
|
2431
|
+
leptris_native_resolve_rb, 1);
|
|
2432
|
+
|
|
2433
|
+
m_native = rb_define_module_under(m_xml, "Native");
|
|
2434
|
+
m_native_sentinel = m_native;
|
|
2435
|
+
rb_define_module_function(m_native, "fast_name", nf_name, 1);
|
|
2436
|
+
rb_define_module_function(m_native, "fast_element_text", nf_element_text, 1);
|
|
2437
|
+
rb_define_module_function(m_native, "fast_attribute", nf_attribute, 2);
|
|
2438
|
+
rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
|
|
2439
|
+
rb_define_module_function(m_native, "fast_attribute2", nf_fast_attribute2, 2);
|
|
2440
|
+
rb_define_module_function(m_native, "ns_lift_needed?", nf_ns_lift_needed, 1);
|
|
2441
|
+
rb_define_module_function(m_native, "append_binding_child",
|
|
2442
|
+
nf_append_binding_child, 3);
|
|
2443
|
+
rb_define_module_function(m_native, "set_binding_attribute",
|
|
2444
|
+
nf_set_binding_attribute, 4);
|
|
2445
|
+
rb_define_module_function(m_native, "insert_binding_child",
|
|
2446
|
+
nf_insert_binding_child, 4);
|
|
2447
|
+
rb_define_module_function(m_native, "at_xpath_first",
|
|
2448
|
+
nf_at_xpath_first, 2);
|
|
2449
|
+
rb_define_module_function(m_native, "materialize_xpath",
|
|
2450
|
+
nf_materialize_xpath, 2);
|
|
2451
|
+
rb_define_module_function(m_native, "set_binding_name",
|
|
2452
|
+
nf_set_binding_name, 3);
|
|
2453
|
+
rb_define_module_function(m_native, "set_binding_text",
|
|
2454
|
+
nf_set_binding_text, 3);
|
|
2455
|
+
rb_define_module_function(m_native, "unlink_binding_node",
|
|
2456
|
+
nf_unlink_binding_node, 2);
|
|
2457
|
+
rb_define_module_function(m_native, "traverse_binding",
|
|
2458
|
+
nf_traverse_binding, 2);
|
|
2459
|
+
rb_define_module_function(m_native, "visit_binding",
|
|
2460
|
+
nf_visit_binding, 2);
|
|
2461
|
+
rb_define_module_function(m_native, "fast_text_content",
|
|
2462
|
+
nf_fast_text_content, 1);
|
|
2463
|
+
rb_define_module_function(m_native, "fast_comment_content",
|
|
2464
|
+
nf_fast_comment_content, 1);
|
|
2465
|
+
rb_define_module_function(m_native, "fast_cdata_content",
|
|
2466
|
+
nf_fast_cdata_content, 1);
|
|
2467
|
+
rb_define_module_function(m_native, "fast_pi_target",
|
|
2468
|
+
nf_fast_pi_target, 1);
|
|
2469
|
+
rb_define_module_function(m_native, "fast_pi_data",
|
|
2470
|
+
nf_fast_pi_data, 1);
|
|
2471
|
+
rb_define_module_function(m_native, "fast_path",
|
|
2472
|
+
nf_fast_path, 1);
|
|
2473
|
+
rb_define_module_function(m_native, "first_element_child",
|
|
2474
|
+
nf_first_element_child, 2);
|
|
2475
|
+
rb_define_module_function(m_native, "last_element_child",
|
|
2476
|
+
nf_last_element_child, 2);
|
|
2477
|
+
rb_define_module_function(m_native, "copy_binding_element",
|
|
2478
|
+
nf_copy_binding_element, 2);
|
|
2479
|
+
rb_define_module_function(m_native, "set_binding_root",
|
|
2480
|
+
nf_set_binding_root, 2);
|
|
2481
|
+
rb_define_module_function(m_native, "parse_binding_document",
|
|
2482
|
+
nf_parse_binding_document, 1);
|
|
2483
|
+
rb_define_module_function(m_native, "parse_fragment_addr",
|
|
2484
|
+
nf_parse_fragment_addr, 2);
|
|
2485
|
+
rb_define_module_function(m_native, "append_markup",
|
|
2486
|
+
nf_append_markup, 3);
|
|
2487
|
+
rb_define_module_function(m_native, "fast_inner_xml",
|
|
2488
|
+
nf_fast_inner_xml, 1);
|
|
2489
|
+
rb_define_module_function(m_native, "snapshot_subtree",
|
|
2490
|
+
nf_snapshot_subtree, 2);
|
|
2491
|
+
rb_define_module_function(m_native, "snapshot_rows",
|
|
2492
|
+
nf_snapshot_rows, 2);
|
|
2493
|
+
rb_define_module_function(m_native, "doc_handle_attach",
|
|
2494
|
+
nf_doc_handle_attach, 1);
|
|
2495
|
+
rb_define_module_function(m_native, "doc_handle_release",
|
|
2496
|
+
nf_doc_handle_release, 1);
|
|
2497
|
+
rb_define_module_function(m_native, "create_binding_document",
|
|
2498
|
+
nf_create_binding_document, 0);
|
|
2499
|
+
c_doc_handle = rb_define_class_under(m_xml, "DocHandle", rb_cObject);
|
|
2500
|
+
rb_undef_alloc_func(c_doc_handle);
|
|
2501
|
+
rb_define_module_function(m_native, "bulk_attributes", nf_bulk_attributes, 1);
|
|
2502
|
+
rb_define_module_function(m_native, "bulk_attr_faces",
|
|
2503
|
+
nf_bulk_attr_faces, 2);
|
|
2504
|
+
rb_define_module_function(m_native, "create_binding_element",
|
|
2505
|
+
nf_create_binding_element, 2);
|
|
2506
|
+
rb_define_module_function(m_native, "create_binding_text",
|
|
2507
|
+
nf_create_binding_text, 2);
|
|
2508
|
+
rb_define_module_function(m_native, "bulk_children", nf_bulk_children, 2);
|
|
2509
|
+
rb_define_module_function(m_native, "bulk_element_children",
|
|
2510
|
+
nf_bulk_element_children, 2);
|
|
2511
|
+
rb_define_module_function(m_native, "bulk_xpath", nf_bulk_xpath, 2);
|
|
2512
|
+
rb_define_module_function(m_native, "fast_document_xml",
|
|
2513
|
+
nf_fast_document_xml, 4);
|
|
2514
|
+
rb_define_module_function(m_native, "fast_element_xml",
|
|
2515
|
+
nf_fast_element_xml, 4);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
#ifdef _WIN32
|
|
2519
|
+
/* MRI derives a C extension's init symbol from the require
|
|
2520
|
+
* feature's basename cut at the first dot. The Windows per-minor
|
|
2521
|
+
* DLLs are therefore named dot-free (native_3_3.so etc.), and
|
|
2522
|
+
* each needs its exact init name exported. All are defined here
|
|
2523
|
+
* unconditionally: a DLL only ever loads under the Ruby minor it
|
|
2524
|
+
* was built against (PE binds its build Ruby's runtime DLL). */
|
|
2525
|
+
void Init_native_3_3(void) { Init_native(); }
|
|
2526
|
+
void Init_native_3_4(void) { Init_native(); }
|
|
2527
|
+
void Init_native_4_0(void) { Init_native(); }
|
|
2528
|
+
#endif
|