makiri 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/conformance.yml +22 -0
- data/.github/workflows/libfuzzer.yml +83 -0
- data/.github/workflows/release.yml +12 -7
- data/.github/workflows/security.yml +88 -3
- data/.github/workflows/valgrind.yml +135 -0
- data/CHANGELOG.md +152 -15
- data/README.md +183 -13
- data/Rakefile +294 -7
- data/ext/makiri/bridge/bridge.h +28 -0
- data/ext/makiri/bridge/ruby_string.c +282 -12
- data/ext/makiri/core/mkr_alloc.c +40 -3
- data/ext/makiri/core/mkr_alloc.h +28 -5
- data/ext/makiri/core/mkr_buf.c +47 -3
- data/ext/makiri/core/mkr_buf.h +112 -3
- data/ext/makiri/core/mkr_core.c +143 -0
- data/ext/makiri/core/mkr_core.h +11 -2
- data/ext/makiri/core/mkr_hash.h +1 -1
- data/ext/makiri/core/mkr_span.h +186 -0
- data/ext/makiri/core/mkr_text.h +8 -8
- data/ext/makiri/core/mkr_utf8.c +101 -0
- data/ext/makiri/core/mkr_utf8.h +88 -0
- data/ext/makiri/extconf.rb +123 -10
- data/ext/makiri/fuzz/Makefile +95 -0
- data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
- data/ext/makiri/fuzz/xml_fuzz.c +24 -0
- data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
- data/ext/makiri/glue/glue.h +55 -11
- data/ext/makiri/glue/ruby_doc.c +129 -59
- data/ext/makiri/glue/ruby_html_css.c +292 -0
- data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
- data/ext/makiri/glue/ruby_html_node.c +859 -0
- data/ext/makiri/glue/ruby_html_serialize.c +154 -0
- data/ext/makiri/glue/ruby_node.c +74 -729
- data/ext/makiri/glue/ruby_node_set.c +167 -32
- data/ext/makiri/glue/ruby_xml.c +602 -0
- data/ext/makiri/glue/ruby_xml_node.c +1373 -0
- data/ext/makiri/glue/ruby_xpath.c +63 -30
- data/ext/makiri/glue/ruby_xpath.h +19 -0
- data/ext/makiri/lexbor_compat/compat.h +42 -9
- data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
- data/ext/makiri/lexbor_compat/dom_index.c +2 -2
- data/ext/makiri/lexbor_compat/post_parse.c +100 -10
- data/ext/makiri/lexbor_compat/source_loc.c +15 -13
- data/ext/makiri/lexbor_compat/text_index.c +14 -8
- data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
- data/ext/makiri/makiri.c +184 -6
- data/ext/makiri/makiri.h +43 -2
- data/ext/makiri/xml/mkr_xml.h +125 -0
- data/ext/makiri/xml/mkr_xml_chars.c +195 -0
- data/ext/makiri/xml/mkr_xml_index.c +169 -0
- data/ext/makiri/xml/mkr_xml_index.h +48 -0
- data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
- data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
- data/ext/makiri/xml/mkr_xml_node.c +399 -0
- data/ext/makiri/xml/mkr_xml_node.h +184 -0
- data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
- data/ext/makiri/xpath/mkr_css.c +1023 -0
- data/ext/makiri/xpath/mkr_css.h +65 -0
- data/ext/makiri/xpath/mkr_xpath.c +96 -32
- data/ext/makiri/xpath/mkr_xpath.h +109 -4
- data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
- data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
- data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
- data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
- data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
- data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
- data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
- data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
- data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
- data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
- data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
- data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
- data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
- data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
- data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
- data/lib/makiri/{attribute.rb → attr.rb} +7 -3
- data/lib/makiri/cdata_section.rb +19 -0
- data/lib/makiri/comment.rb +10 -0
- data/lib/makiri/compat_aliases.rb +30 -0
- data/lib/makiri/document.rb +9 -73
- data/lib/makiri/document_fragment.rb +14 -9
- data/lib/makiri/element.rb +4 -4
- data/lib/makiri/html/document.rb +106 -0
- data/lib/makiri/html/node_methods.rb +19 -0
- data/lib/makiri/html.rb +12 -0
- data/lib/makiri/node.rb +58 -15
- data/lib/makiri/node_set.rb +8 -0
- data/lib/makiri/processing_instruction.rb +10 -0
- data/lib/makiri/text.rb +1 -1
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/builder.rb +263 -0
- data/lib/makiri/xml/document.rb +24 -0
- data/lib/makiri/xml/node_methods.rb +84 -0
- data/lib/makiri/xml.rb +10 -0
- data/lib/makiri/xpath_context.rb +1 -1
- data/lib/makiri.rb +24 -5
- data/script/build_native_gem.rb +2 -2
- data/script/check_alloc_failures.rb +266 -0
- data/script/check_c_safety.rb +77 -2
- data/script/check_c_safety_allowlist.yml +102 -0
- data/script/check_leaks.rb +64 -0
- data/script/leaks_harness.rb +64 -0
- data/vendor/lexbor/CMakeLists.txt +6 -0
- data/vendor/lexbor/README.md +12 -0
- data/vendor/lexbor/config.cmake +1 -1
- data/vendor/lexbor/source/lexbor/core/base.h +1 -1
- data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
- data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
- data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
- data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
- data/vendor/lexbor/source/lexbor/html/base.h +1 -1
- data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
- data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
- data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
- data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
- data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
- data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
- data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
- data/vendor/lexbor/source/lexbor/url/base.h +1 -1
- data/vendor/lexbor/source/lexbor/url/url.c +5 -2
- data/vendor/lexbor/source/lexbor/url/url.h +9 -0
- data/vendor/lexbor/version +1 -1
- metadata +53 -9
- data/ext/makiri/glue/ruby_css.c +0 -185
- data/ext/makiri/glue/ruby_serialize.c +0 -92
- data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
- data/lib/makiri/cdata.rb +0 -6
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
* native context. Phase 1 implements: child / attribute / self /
|
|
14
14
|
* descendant-or-self / parent axes; predicates with position; the
|
|
15
15
|
* full set of XPath 1.0 binary operators (semantically); arithmetic;
|
|
16
|
-
* and a small built-in function table (see
|
|
16
|
+
* and a small built-in function table (see mkr_xpath_funcs_body.h).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/* ---------- forward decls ---------- */
|
|
20
20
|
|
|
21
21
|
static int eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
22
|
-
|
|
22
|
+
const mkr_focus_t *focus,
|
|
23
23
|
mkr_val_t *out, mkr_xpath_error_t *err);
|
|
24
24
|
|
|
25
25
|
/* ---------- node test ---------- */
|
|
@@ -29,49 +29,126 @@ static int eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
29
29
|
* explicit namespace (LXB_NS__UNDEF). For other ids it looks up the URI
|
|
30
30
|
* in the document's ns hash. */
|
|
31
31
|
static mkr_borrowed_text_t
|
|
32
|
-
node_ns_text(
|
|
32
|
+
node_ns_text(MKR_DOM_NODE *node, MKR_DOM_DOCUMENT *doc)
|
|
33
33
|
{
|
|
34
|
-
if (node->ns == LXB_NS__UNDEF) return mkr_borrowed_text_lit("");
|
|
35
|
-
if (doc == NULL || doc->ns == NULL) return mkr_borrowed_text_lit("");
|
|
36
34
|
size_t len = 0;
|
|
37
|
-
const
|
|
38
|
-
return uri ? mkr_borrowed_text(
|
|
39
|
-
: mkr_borrowed_text_lit("");
|
|
35
|
+
const char *uri = MKR_NODE_NS_URI(node, doc, &len);
|
|
36
|
+
return uri ? mkr_borrowed_text(uri, len) : mkr_borrowed_text_lit("");
|
|
40
37
|
}
|
|
41
38
|
|
|
39
|
+
/* Host-specific element/attribute name-test match (§8.6). The principal-node-type
|
|
40
|
+
* filter has already passed (node is an element, or an attribute on the attribute
|
|
41
|
+
* axis); this decides whether its name and namespace match +test+.
|
|
42
|
+
*
|
|
43
|
+
* HTML uses the qualified-name model: an unprefixed test compares the node's
|
|
44
|
+
* qualified name (== local name for HTML, which has no element prefixes) and, in
|
|
45
|
+
* strict mode, restricts elements to the HTML/no namespace; a prefixed test
|
|
46
|
+
* compares the local name plus the namespace URI bound to the prefix. The XML
|
|
47
|
+
* instance defines MKR_HOST_XML and provides its own local-name + ns_uri match. */
|
|
48
|
+
/* +have_pre+ supplies the prefix's already-resolved namespace URI (pre_uri /
|
|
49
|
+
* pre_uri_len), so a hot multi-node walk resolves the test prefix once in
|
|
50
|
+
* eval_step instead of per node. have_pre == 0 resolves it here as before. */
|
|
42
51
|
static int
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
name_test_match(const mkr_nodetest_t *test, MKR_DOM_NODE *node,
|
|
53
|
+
mkr_axis_t axis, mkr_xpath_context_t *ctx,
|
|
54
|
+
const char *pre_uri, size_t pre_uri_len, int have_pre)
|
|
55
|
+
{
|
|
56
|
+
#ifdef MKR_HOST_XML
|
|
57
|
+
/* XML name match (local name + namespace URI). An unprefixed test matches a
|
|
58
|
+
* no-namespace node only (a prefixed or default-namespaced element needs a
|
|
59
|
+
* registered prefix, §8.6); a prefixed test matches the local name plus the
|
|
60
|
+
* URI bound to the prefix. There is no qualified-name buffer on the custom
|
|
61
|
+
* node, so the comparison is always against the local name. */
|
|
62
|
+
size_t got_len = 0;
|
|
63
|
+
const lxb_char_t *got = (axis == MKR_AXIS_ATTRIBUTE)
|
|
64
|
+
? MKR_ATTR_LOCAL_NAME(node, &got_len)
|
|
65
|
+
: MKR_ELEM_LOCAL_NAME(node, &got_len);
|
|
66
|
+
if (got == NULL || test->local.ptr == NULL) return 0;
|
|
67
|
+
if (!mkr_borrowed_text_eq(mkr_borrowed_text_from_owned(test->local),
|
|
68
|
+
mkr_borrowed_text((const char *)got, got_len))) return 0;
|
|
69
|
+
|
|
70
|
+
size_t node_uri_len = 0;
|
|
71
|
+
const char *node_uri = MKR_NODE_NS_URI(node, mkr_ctx_document(ctx), &node_uri_len);
|
|
72
|
+
if (test->prefix.ptr != NULL) {
|
|
73
|
+
size_t want_uri_len = pre_uri_len;
|
|
74
|
+
const char *want_uri = have_pre ? pre_uri
|
|
75
|
+
: mkr_ctx_lookup_ns(ctx, test->prefix.ptr, test->prefix.len, &want_uri_len);
|
|
76
|
+
if (want_uri == NULL) return 0;
|
|
77
|
+
if (!mkr_bytes_eq(want_uri, want_uri_len, node_uri, node_uri_len)) return 0;
|
|
78
|
+
} else if (node_uri_len != 0 && !mkr_ctx_unprefixed_lax(ctx)) {
|
|
79
|
+
/* unprefixed test, strict: the node must be in no namespace */
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
return 1;
|
|
83
|
+
#else
|
|
84
|
+
/* HTML name match (qualified-name model). */
|
|
85
|
+
size_t got_len = 0;
|
|
86
|
+
const lxb_char_t *got;
|
|
87
|
+
if (test->prefix.ptr != NULL) {
|
|
88
|
+
got = (axis == MKR_AXIS_ATTRIBUTE) ? MKR_ATTR_LOCAL_NAME(node, &got_len)
|
|
89
|
+
: MKR_ELEM_LOCAL_NAME(node, &got_len);
|
|
90
|
+
} else {
|
|
91
|
+
got = (axis == MKR_AXIS_ATTRIBUTE) ? MKR_ATTR_QUALIFIED_NAME(node, &got_len)
|
|
92
|
+
: MKR_ELEM_QUALIFIED_NAME(node, &got_len);
|
|
93
|
+
}
|
|
94
|
+
if (got == NULL || test->local.ptr == NULL) return 0;
|
|
95
|
+
if (!mkr_borrowed_text_eq(mkr_borrowed_text_from_owned(test->local),
|
|
96
|
+
mkr_borrowed_text((const char *)got, got_len))) return 0;
|
|
97
|
+
|
|
98
|
+
if (test->prefix.ptr != NULL) {
|
|
99
|
+
size_t want_uri_len = pre_uri_len;
|
|
100
|
+
const char *want_uri = have_pre ? pre_uri
|
|
101
|
+
: mkr_ctx_lookup_ns(ctx, test->prefix.ptr, test->prefix.len, &want_uri_len);
|
|
102
|
+
if (want_uri == NULL) return 0; /* unknown prefix -> non-match (step driver reports) */
|
|
103
|
+
mkr_borrowed_text_t node_uri = node_ns_text(node, mkr_ctx_document(ctx));
|
|
104
|
+
if (!mkr_bytes_eq(want_uri, want_uri_len, node_uri.ptr, node_uri.len)) return 0;
|
|
105
|
+
} else if (!mkr_ctx_unprefixed_lax(ctx)
|
|
106
|
+
&& axis != MKR_AXIS_ATTRIBUTE
|
|
107
|
+
&& MKR_NODE_IS_FOREIGN_NS(node)) {
|
|
108
|
+
/* strict mode: unprefixed ELEMENT tests resolve in the HTML namespace, so a
|
|
109
|
+
* foreign (SVG/MathML) element needs a prefix. Attributes are exempt (an
|
|
110
|
+
* unprefixed attribute test matches by no-namespace local name; the
|
|
111
|
+
* qualified-name compare above already excludes prefixed foreign attrs). */
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
return 1;
|
|
115
|
+
#endif /* MKR_HOST_XML */
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static int
|
|
119
|
+
node_principal_match_pre(const mkr_nodetest_t *test, MKR_DOM_NODE *node,
|
|
120
|
+
mkr_axis_t axis, mkr_xpath_context_t *ctx,
|
|
121
|
+
const char *pre_uri, size_t pre_uri_len, int have_pre)
|
|
45
122
|
{
|
|
46
123
|
switch (test->kind) {
|
|
47
124
|
case MKR_NT_NODE:
|
|
48
125
|
/* XPath 1.0 §5 data model: only element, attribute, text,
|
|
49
126
|
* namespace, processing-instruction, comment, and the root are
|
|
50
127
|
* model nodes. Lexbor additionally exposes DOCUMENT_TYPE, ENTITY,
|
|
51
|
-
* ENTITY_REFERENCE, and NOTATION nodes
|
|
128
|
+
* ENTITY_REFERENCE, and NOTATION nodes - none belong in the
|
|
52
129
|
* model, so node() must not match them. DocumentFragment IS
|
|
53
130
|
* matched: it acts as the root for fragment-rooted contexts,
|
|
54
131
|
* so '.' / 'self::node()' over a fragment has to see it. */
|
|
55
|
-
switch (node
|
|
56
|
-
case
|
|
57
|
-
case
|
|
58
|
-
case
|
|
59
|
-
case
|
|
132
|
+
switch (MKR_NODE_TYPE(node)) {
|
|
133
|
+
case MKR_NTYPE_DOCUMENT_TYPE:
|
|
134
|
+
case MKR_NTYPE_ENTITY:
|
|
135
|
+
case MKR_NTYPE_ENTITY_REFERENCE:
|
|
136
|
+
case MKR_NTYPE_NOTATION:
|
|
60
137
|
return 0;
|
|
61
138
|
default:
|
|
62
139
|
return 1;
|
|
63
140
|
}
|
|
64
141
|
case MKR_NT_TEXT:
|
|
65
|
-
return node
|
|
66
|
-
|| node
|
|
142
|
+
return MKR_NODE_TYPE(node) == MKR_NTYPE_TEXT
|
|
143
|
+
|| MKR_NODE_TYPE(node) == MKR_NTYPE_CDATA_SECTION;
|
|
67
144
|
case MKR_NT_COMMENT:
|
|
68
|
-
return node
|
|
145
|
+
return MKR_NODE_TYPE(node) == MKR_NTYPE_COMMENT;
|
|
69
146
|
case MKR_NT_PI:
|
|
70
|
-
if (node
|
|
147
|
+
if (MKR_NODE_TYPE(node) != MKR_NTYPE_PI) return 0;
|
|
71
148
|
if (test->pi_target.ptr == NULL) return 1;
|
|
72
149
|
{
|
|
73
150
|
size_t nlen = 0;
|
|
74
|
-
const lxb_char_t *nm =
|
|
151
|
+
const lxb_char_t *nm = MKR_NODE_PI_NAME(node, &nlen);
|
|
75
152
|
return mkr_borrowed_text_eq(mkr_borrowed_text_from_owned(test->pi_target),
|
|
76
153
|
mkr_borrowed_text((const char *)nm, nlen));
|
|
77
154
|
}
|
|
@@ -80,183 +157,139 @@ node_principal_match(const mkr_nodetest_t *test, lxb_dom_node_t *node,
|
|
|
80
157
|
return 0; /* see internal.h §6 */
|
|
81
158
|
/* Principal node type. */
|
|
82
159
|
if (axis == MKR_AXIS_ATTRIBUTE) {
|
|
83
|
-
if (node
|
|
84
|
-
} else if (node
|
|
160
|
+
if (MKR_NODE_TYPE(node) != MKR_NTYPE_ATTRIBUTE) return 0;
|
|
161
|
+
} else if (MKR_NODE_TYPE(node) != MKR_NTYPE_ELEMENT) {
|
|
85
162
|
return 0;
|
|
86
163
|
}
|
|
87
164
|
/* `*` matches any namespace; `prefix:*` matches only the namespace bound
|
|
88
165
|
* to prefix (internal.h §5). Unknown prefix is reported up front by the
|
|
89
166
|
* step driver; here it is a non-match. */
|
|
90
167
|
if (test->prefix.ptr != NULL) {
|
|
91
|
-
size_t want_uri_len =
|
|
92
|
-
const char *want_uri =
|
|
168
|
+
size_t want_uri_len = pre_uri_len;
|
|
169
|
+
const char *want_uri = have_pre ? pre_uri
|
|
170
|
+
: mkr_ctx_lookup_ns(ctx, test->prefix.ptr, test->prefix.len, &want_uri_len);
|
|
93
171
|
if (want_uri == NULL) return 0;
|
|
94
172
|
mkr_borrowed_text_t node_uri = node_ns_text(node, mkr_ctx_document(ctx));
|
|
95
|
-
if (
|
|
173
|
+
if (!mkr_bytes_eq(want_uri, want_uri_len, node_uri.ptr, node_uri.len)) return 0;
|
|
96
174
|
}
|
|
97
175
|
return 1;
|
|
98
176
|
}
|
|
99
|
-
case MKR_NT_NAME:
|
|
177
|
+
case MKR_NT_NAME:
|
|
100
178
|
/* Principal node type filter. */
|
|
101
179
|
if (axis == MKR_AXIS_ATTRIBUTE) {
|
|
102
|
-
if (node
|
|
103
|
-
} else if (node
|
|
104
|
-
return 0;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/* Local-name comparison. For prefixed tests we compare local names
|
|
108
|
-
* (no prefix); for unprefixed tests we compare against the
|
|
109
|
-
* qualified name, which for HTML is equivalent to the local name. */
|
|
110
|
-
size_t got_len = 0;
|
|
111
|
-
const lxb_char_t *got;
|
|
112
|
-
if (test->prefix.ptr != NULL) {
|
|
113
|
-
if (axis == MKR_AXIS_ATTRIBUTE) {
|
|
114
|
-
got = lxb_dom_attr_local_name((lxb_dom_attr_t *)node, &got_len);
|
|
115
|
-
} else {
|
|
116
|
-
got = lxb_dom_element_local_name((lxb_dom_element_t *)node, &got_len);
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
if (axis == MKR_AXIS_ATTRIBUTE) {
|
|
120
|
-
got = lxb_dom_attr_qualified_name((lxb_dom_attr_t *)node, &got_len);
|
|
121
|
-
} else {
|
|
122
|
-
got = mkr_dom_node_name_qualified(node, &got_len);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (got == NULL) return 0;
|
|
126
|
-
if (test->local.ptr == NULL) return 0;
|
|
127
|
-
if (!mkr_borrowed_text_eq(mkr_borrowed_text_from_owned(test->local),
|
|
128
|
-
mkr_borrowed_text((const char *)got, got_len))) return 0;
|
|
129
|
-
|
|
130
|
-
/* Namespace check (see internal.h §2–§4).
|
|
131
|
-
* - Prefixed test: the node's namespace URI must equal the URI bound to
|
|
132
|
-
* the prefix.
|
|
133
|
-
* - Unprefixed test, strict (default, HTML5/WHATWG-faithful): the name
|
|
134
|
-
* resolves in the HTML namespace, so it matches only HTML-namespace or
|
|
135
|
-
* no-namespace nodes — foreign (SVG/MathML) content needs a prefix.
|
|
136
|
-
* This mirrors browsers' document.evaluate and Nokogiri::HTML5.
|
|
137
|
-
* - Unprefixed test, lax: namespace ignored (matched by local name
|
|
138
|
-
* already), the HTML4 / Nokogiri::HTML-style convenience. */
|
|
139
|
-
if (test->prefix.ptr != NULL) {
|
|
140
|
-
size_t want_uri_len = 0;
|
|
141
|
-
const char *want_uri = mkr_ctx_lookup_ns(ctx, test->prefix.ptr, test->prefix.len, &want_uri_len);
|
|
142
|
-
if (want_uri == NULL) {
|
|
143
|
-
/* Unknown prefix: leave for the step driver to report so the
|
|
144
|
-
* error is uniform across all hit nodes. We treat it as
|
|
145
|
-
* non-match here; node_principal_match cannot raise on its own. */
|
|
146
|
-
return 0;
|
|
147
|
-
}
|
|
148
|
-
mkr_borrowed_text_t node_uri = node_ns_text(node, mkr_ctx_document(ctx));
|
|
149
|
-
if (want_uri_len != node_uri.len || memcmp(want_uri, node_uri.ptr, want_uri_len) != 0) return 0;
|
|
150
|
-
} else if (!mkr_ctx_unprefixed_lax(ctx)
|
|
151
|
-
&& axis != MKR_AXIS_ATTRIBUTE
|
|
152
|
-
&& node->ns != LXB_NS_HTML && node->ns != LXB_NS__UNDEF) {
|
|
153
|
-
/* Strict mode restricts ELEMENT name tests to the HTML namespace; foreign
|
|
154
|
-
* (SVG/MathML) elements need a prefix. Attributes are exempt: an
|
|
155
|
-
* unprefixed attribute test matches by no-namespace local name (the DOM
|
|
156
|
-
* model — `id` on an <svg> is still a null-namespace attribute), and the
|
|
157
|
-
* qualified-name comparison above already excludes prefixed foreign
|
|
158
|
-
* attributes like xlink:href. Lexbor tags attributes with their element's
|
|
159
|
-
* ns, which does not reflect the DOM, so we must not gate attributes on
|
|
160
|
-
* node->ns here. */
|
|
180
|
+
if (MKR_NODE_TYPE(node) != MKR_NTYPE_ATTRIBUTE) return 0;
|
|
181
|
+
} else if (MKR_NODE_TYPE(node) != MKR_NTYPE_ELEMENT) {
|
|
161
182
|
return 0;
|
|
162
183
|
}
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
/* The local-name + namespace match for an element/attribute name test is
|
|
185
|
+
* host-specific (HTML's qualified-name model vs XML's local+namespace-URI
|
|
186
|
+
* model, §8.6), so it lives in name_test_match - defined per instance. */
|
|
187
|
+
return name_test_match(test, node, axis, ctx, pre_uri, pre_uri_len, have_pre);
|
|
165
188
|
}
|
|
166
189
|
return 0;
|
|
167
190
|
}
|
|
168
191
|
|
|
192
|
+
/* Behaviour-preserving wrapper: resolve the prefix per call, as before. Used by
|
|
193
|
+
* the first-match path and the //tag index re-check (not hot enough to pre-
|
|
194
|
+
* resolve); the hot step walk calls node_principal_match_pre directly. */
|
|
195
|
+
static int
|
|
196
|
+
node_principal_match(const mkr_nodetest_t *test, MKR_DOM_NODE *node,
|
|
197
|
+
mkr_axis_t axis, mkr_xpath_context_t *ctx)
|
|
198
|
+
{
|
|
199
|
+
return node_principal_match_pre(test, node, axis, ctx, NULL, 0, 0);
|
|
200
|
+
}
|
|
201
|
+
|
|
169
202
|
/* ---------- axis walkers ---------- */
|
|
170
203
|
|
|
171
204
|
static int
|
|
172
|
-
walk_axis(mkr_axis_t axis,
|
|
173
|
-
int (*visit)(
|
|
205
|
+
walk_axis(mkr_axis_t axis, MKR_DOM_NODE *context,
|
|
206
|
+
int (*visit)(MKR_DOM_NODE *n, void *u), void *u)
|
|
174
207
|
{
|
|
175
208
|
switch (axis) {
|
|
176
209
|
case MKR_AXIS_SELF:
|
|
177
210
|
if (visit(context, u)) return 1;
|
|
178
211
|
return 0;
|
|
179
212
|
case MKR_AXIS_PARENT: {
|
|
180
|
-
|
|
213
|
+
MKR_DOM_NODE *p = MKR_NODE_PARENT(context);
|
|
181
214
|
if (p && visit(p, u)) return 1;
|
|
182
215
|
return 0;
|
|
183
216
|
}
|
|
184
217
|
case MKR_AXIS_CHILD:
|
|
185
|
-
for (
|
|
218
|
+
for (MKR_DOM_NODE *c = MKR_NODE_FIRST_CHILD(context); c != NULL; c = MKR_NODE_NEXT(c)) {
|
|
186
219
|
if (visit(c, u)) return 1;
|
|
187
220
|
}
|
|
188
221
|
return 0;
|
|
189
222
|
case MKR_AXIS_ATTRIBUTE: {
|
|
190
|
-
if (context
|
|
191
|
-
|
|
192
|
-
for (
|
|
193
|
-
if (visit((
|
|
223
|
+
if (MKR_NODE_TYPE(context) != MKR_NTYPE_ELEMENT) return 0;
|
|
224
|
+
MKR_DOM_ELEMENT *el = (MKR_DOM_ELEMENT *)context;
|
|
225
|
+
for (MKR_DOM_ATTR *a = MKR_ELEM_FIRST_ATTR(el); a != NULL; a = MKR_ATTR_NEXT(a)) {
|
|
226
|
+
if (visit((MKR_DOM_NODE *)a, u)) return 1;
|
|
194
227
|
}
|
|
195
228
|
return 0;
|
|
196
229
|
}
|
|
197
230
|
case MKR_AXIS_DESCENDANT_OR_SELF: {
|
|
198
231
|
/* DFS pre-order. */
|
|
199
232
|
if (visit(context, u)) return 1;
|
|
200
|
-
|
|
233
|
+
MKR_DOM_NODE *n = MKR_NODE_FIRST_CHILD(context);
|
|
201
234
|
while (n != NULL && n != context) {
|
|
202
235
|
if (visit(n, u)) return 1;
|
|
203
|
-
if (n
|
|
204
|
-
n = n
|
|
236
|
+
if (MKR_NODE_FIRST_CHILD(n)) {
|
|
237
|
+
n = MKR_NODE_FIRST_CHILD(n);
|
|
205
238
|
} else {
|
|
206
|
-
while (n != context && n
|
|
239
|
+
while (n != context && MKR_NODE_NEXT(n) == NULL) n = MKR_NODE_PARENT(n);
|
|
207
240
|
if (n == context) break;
|
|
208
|
-
n = n
|
|
241
|
+
n = MKR_NODE_NEXT(n);
|
|
209
242
|
}
|
|
210
243
|
}
|
|
211
244
|
return 0;
|
|
212
245
|
}
|
|
213
246
|
case MKR_AXIS_DESCENDANT: {
|
|
214
|
-
|
|
247
|
+
MKR_DOM_NODE *n = MKR_NODE_FIRST_CHILD(context);
|
|
215
248
|
while (n != NULL && n != context) {
|
|
216
249
|
if (visit(n, u)) return 1;
|
|
217
|
-
if (n
|
|
218
|
-
n = n
|
|
250
|
+
if (MKR_NODE_FIRST_CHILD(n)) {
|
|
251
|
+
n = MKR_NODE_FIRST_CHILD(n);
|
|
219
252
|
} else {
|
|
220
|
-
while (n != context && n
|
|
253
|
+
while (n != context && MKR_NODE_NEXT(n) == NULL) n = MKR_NODE_PARENT(n);
|
|
221
254
|
if (n == context) break;
|
|
222
|
-
n = n
|
|
255
|
+
n = MKR_NODE_NEXT(n);
|
|
223
256
|
}
|
|
224
257
|
}
|
|
225
258
|
return 0;
|
|
226
259
|
}
|
|
227
260
|
case MKR_AXIS_ANCESTOR:
|
|
228
|
-
for (
|
|
261
|
+
for (MKR_DOM_NODE *p = MKR_NODE_PARENT(context); p != NULL; p = MKR_NODE_PARENT(p)) {
|
|
229
262
|
if (visit(p, u)) return 1;
|
|
230
263
|
}
|
|
231
264
|
return 0;
|
|
232
265
|
case MKR_AXIS_ANCESTOR_OR_SELF:
|
|
233
|
-
for (
|
|
266
|
+
for (MKR_DOM_NODE *p = context; p != NULL; p = MKR_NODE_PARENT(p)) {
|
|
234
267
|
if (visit(p, u)) return 1;
|
|
235
268
|
}
|
|
236
269
|
return 0;
|
|
237
270
|
case MKR_AXIS_FOLLOWING_SIBLING:
|
|
238
|
-
for (
|
|
271
|
+
for (MKR_DOM_NODE *s = MKR_NODE_NEXT(context); s != NULL; s = MKR_NODE_NEXT(s)) {
|
|
239
272
|
if (visit(s, u)) return 1;
|
|
240
273
|
}
|
|
241
274
|
return 0;
|
|
242
275
|
case MKR_AXIS_PRECEDING_SIBLING:
|
|
243
|
-
for (
|
|
276
|
+
for (MKR_DOM_NODE *s = MKR_NODE_PREV(context); s != NULL; s = MKR_NODE_PREV(s)) {
|
|
244
277
|
if (visit(s, u)) return 1;
|
|
245
278
|
}
|
|
246
279
|
return 0;
|
|
247
280
|
case MKR_AXIS_FOLLOWING: {
|
|
248
281
|
/* Start at the next node in doc order after context's subtree. */
|
|
249
|
-
|
|
250
|
-
while (cur != NULL && cur
|
|
282
|
+
MKR_DOM_NODE *cur = context;
|
|
283
|
+
while (cur != NULL && MKR_NODE_NEXT(cur) == NULL) cur = MKR_NODE_PARENT(cur);
|
|
251
284
|
if (cur == NULL) return 0;
|
|
252
|
-
cur = cur
|
|
285
|
+
cur = MKR_NODE_NEXT(cur);
|
|
253
286
|
while (cur != NULL) {
|
|
254
287
|
if (visit(cur, u)) return 1;
|
|
255
|
-
if (cur
|
|
256
|
-
cur = cur
|
|
288
|
+
if (MKR_NODE_FIRST_CHILD(cur)) {
|
|
289
|
+
cur = MKR_NODE_FIRST_CHILD(cur);
|
|
257
290
|
} else {
|
|
258
|
-
while (cur != NULL && cur
|
|
259
|
-
if (cur != NULL) cur = cur
|
|
291
|
+
while (cur != NULL && MKR_NODE_NEXT(cur) == NULL) cur = MKR_NODE_PARENT(cur);
|
|
292
|
+
if (cur != NULL) cur = MKR_NODE_NEXT(cur);
|
|
260
293
|
}
|
|
261
294
|
}
|
|
262
295
|
return 0;
|
|
@@ -269,18 +302,18 @@ walk_axis(mkr_axis_t axis, lxb_dom_node_t *context,
|
|
|
269
302
|
* ancestor of context: it's an ancestor only when we're climbing
|
|
270
303
|
* the chain from context itself, not when we're climbing back out
|
|
271
304
|
* of a preceding sibling's subtree. */
|
|
272
|
-
|
|
305
|
+
MKR_DOM_NODE *cur = context;
|
|
273
306
|
while (cur != NULL) {
|
|
274
|
-
if (cur
|
|
275
|
-
cur = cur
|
|
276
|
-
while (cur
|
|
307
|
+
if (MKR_NODE_PREV(cur)) {
|
|
308
|
+
cur = MKR_NODE_PREV(cur);
|
|
309
|
+
while (MKR_NODE_LAST_CHILD(cur)) cur = MKR_NODE_LAST_CHILD(cur);
|
|
277
310
|
if (visit(cur, u)) return 1;
|
|
278
311
|
} else {
|
|
279
|
-
cur = cur
|
|
312
|
+
cur = MKR_NODE_PARENT(cur);
|
|
280
313
|
if (cur == NULL) return 0;
|
|
281
314
|
/* Is cur an ancestor of the original context? */
|
|
282
315
|
int is_ancestor = 0;
|
|
283
|
-
for (
|
|
316
|
+
for (MKR_DOM_NODE *p = MKR_NODE_PARENT(context); p != NULL; p = MKR_NODE_PARENT(p)) {
|
|
284
317
|
if (p == cur) { is_ancestor = 1; break; }
|
|
285
318
|
}
|
|
286
319
|
if (!is_ancestor) {
|
|
@@ -308,7 +341,7 @@ walk_axis(mkr_axis_t axis, lxb_dom_node_t *context,
|
|
|
308
341
|
* throwaway node-set (malloc/free) per context node for the attribute path plus
|
|
309
342
|
* a string-cache insert; recognising the shape lets us filter with a direct
|
|
310
343
|
* attribute lookup instead. Both shapes are boolean (no position dependence),
|
|
311
|
-
* so this is a pure per-node filter
|
|
344
|
+
* so this is a pure per-node filter - identical result to the generic path.
|
|
312
345
|
*/
|
|
313
346
|
typedef struct {
|
|
314
347
|
const char *name;
|
|
@@ -319,7 +352,7 @@ typedef struct {
|
|
|
319
352
|
} mkr_attr_pred_t;
|
|
320
353
|
|
|
321
354
|
/* Shape A: a relative path that is a single unprefixed attribute name test with
|
|
322
|
-
* no predicates
|
|
355
|
+
* no predicates - i.e. `@name`. Fills name/name_len; returns 1 on match. */
|
|
323
356
|
static int
|
|
324
357
|
mkr_match_attr_step(const mkr_node_t *n, const char **name, size_t *name_len)
|
|
325
358
|
{
|
|
@@ -371,23 +404,43 @@ mkr_match_attr_pred(const mkr_node_t *p, mkr_attr_pred_t *out)
|
|
|
371
404
|
/* Find an element's attribute whose qualified name equals +name+ exactly
|
|
372
405
|
* (case-SENSITIVE), or NULL. We scan rather than calling
|
|
373
406
|
* lxb_dom_element_get_attribute, because that does an HTML case-INsensitive
|
|
374
|
-
* lookup
|
|
407
|
+
* lookup - which would make `[@Id]` match `id`, diverging from XPath 1.0, from
|
|
375
408
|
* Nokogiri::HTML5, AND from Makiri's own case-sensitive attribute-axis name
|
|
376
409
|
* test (node_principal_match compares the qualified name byte-for-byte). The
|
|
377
410
|
* fast path only handles unprefixed names, matching that comparison. */
|
|
378
|
-
static
|
|
379
|
-
mkr_attr_by_qualified_name(
|
|
411
|
+
static MKR_DOM_ATTR *
|
|
412
|
+
mkr_attr_by_qualified_name(MKR_DOM_ELEMENT *el, const char *name, size_t name_len)
|
|
380
413
|
{
|
|
381
|
-
for (
|
|
414
|
+
for (MKR_DOM_ATTR *a = MKR_ELEM_FIRST_ATTR(el); a != NULL; a = MKR_ATTR_NEXT(a)) {
|
|
382
415
|
size_t qlen = 0;
|
|
383
|
-
const lxb_char_t *q =
|
|
384
|
-
if (q != NULL &&
|
|
416
|
+
const lxb_char_t *q = MKR_ATTR_QUALIFIED_NAME(a, &qlen);
|
|
417
|
+
if (q != NULL && mkr_bytes_eq(q, qlen, name, name_len)) {
|
|
385
418
|
return a;
|
|
386
419
|
}
|
|
387
420
|
}
|
|
388
421
|
return NULL;
|
|
389
422
|
}
|
|
390
423
|
|
|
424
|
+
/* Does element node +n+ satisfy the recognised attribute predicate +ap+
|
|
425
|
+
* ([@name] or [@name='value'])? This is THE single per-node test - shared by
|
|
426
|
+
* the predicate filter (mkr_filter_attr_pred) and the at_xpath first-match path
|
|
427
|
+
* (mkr_first_node_ok) so the two stay byte-identical by construction rather than
|
|
428
|
+
* by a hand-kept copy. Position-independent (no context size/position), matching
|
|
429
|
+
* the recogniser's "boolean predicate only" contract. */
|
|
430
|
+
static int
|
|
431
|
+
mkr_attr_pred_matches_node(const mkr_attr_pred_t *ap, MKR_DOM_NODE *n)
|
|
432
|
+
{
|
|
433
|
+
if (MKR_NODE_TYPE(n) != MKR_NTYPE_ELEMENT) return 0;
|
|
434
|
+
MKR_DOM_ATTR *a =
|
|
435
|
+
mkr_attr_by_qualified_name(MKR_NODE_AS_ELEMENT(n), ap->name, ap->name_len);
|
|
436
|
+
if (a == NULL) return 0;
|
|
437
|
+
if (!ap->has_value) return 1;
|
|
438
|
+
size_t got_len = 0;
|
|
439
|
+
const lxb_char_t *got = MKR_ATTR_VALUE(a, &got_len);
|
|
440
|
+
if (got == NULL) got_len = 0;
|
|
441
|
+
return mkr_bytes_eq(got, got_len, ap->value, ap->value_len);
|
|
442
|
+
}
|
|
443
|
+
|
|
391
444
|
/* Filter `inout` by a recognised attribute predicate into `kept`. */
|
|
392
445
|
static int
|
|
393
446
|
mkr_filter_attr_pred(mkr_xpath_context_t *ctx, const mkr_attr_pred_t *ap,
|
|
@@ -395,27 +448,13 @@ mkr_filter_attr_pred(mkr_xpath_context_t *ctx, const mkr_attr_pred_t *ap,
|
|
|
395
448
|
mkr_xpath_error_t *err)
|
|
396
449
|
{
|
|
397
450
|
for (size_t i = 0; i < inout->count; ++i) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
keep = 1;
|
|
406
|
-
} else {
|
|
407
|
-
size_t got_len = 0;
|
|
408
|
-
const lxb_char_t *got = lxb_dom_attr_value(a, &got_len);
|
|
409
|
-
if (got == NULL) {
|
|
410
|
-
got_len = 0;
|
|
411
|
-
}
|
|
412
|
-
keep = (got_len == ap->value_len
|
|
413
|
-
&& (ap->value_len == 0
|
|
414
|
-
|| memcmp(got, ap->value, ap->value_len) == 0));
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (keep && mkr_nodeset_push(kept, n, mkr_ctx_limits(ctx), err) != 0) {
|
|
451
|
+
/* Per-candidate op charge: this fast path replaces a per-node generic
|
|
452
|
+
* predicate eval (which would tick via eval_node), so tick here too to keep
|
|
453
|
+
* the [@a]/[@a='v'] filter under the same budget as the path it shortcuts. */
|
|
454
|
+
if (mkr_limit_eval_op(mkr_ctx_limits(ctx), err) != 0) return -1;
|
|
455
|
+
MKR_DOM_NODE *n = inout->items[i];
|
|
456
|
+
if (mkr_attr_pred_matches_node(ap, n)
|
|
457
|
+
&& mkr_nodeset_push(kept, n, mkr_ctx_limits(ctx), err) != 0) {
|
|
419
458
|
return -1;
|
|
420
459
|
}
|
|
421
460
|
}
|
|
@@ -432,7 +471,7 @@ apply_predicates(mkr_xpath_context_t *ctx,
|
|
|
432
471
|
mkr_nodeset_t kept;
|
|
433
472
|
mkr_nodeset_init(&kept);
|
|
434
473
|
|
|
435
|
-
/* Specialise [@name] / [@name='lit']
|
|
474
|
+
/* Specialise [@name] / [@name='lit'] - a position-independent filter, so
|
|
436
475
|
* applying it per predicate (even amid others) matches the generic path. */
|
|
437
476
|
mkr_attr_pred_t ap;
|
|
438
477
|
if (mkr_match_attr_pred(preds[p], &ap)) {
|
|
@@ -448,7 +487,8 @@ apply_predicates(mkr_xpath_context_t *ctx,
|
|
|
448
487
|
size_t size = inout->count;
|
|
449
488
|
for (size_t i = 0; i < size; ++i) {
|
|
450
489
|
mkr_val_t v = {0};
|
|
451
|
-
|
|
490
|
+
mkr_focus_t pf = { inout->items[i], i + 1, size };
|
|
491
|
+
if (eval_node(ctx, preds[p], &pf, &v, err) != 0) {
|
|
452
492
|
mkr_nodeset_clear(&kept);
|
|
453
493
|
return -1;
|
|
454
494
|
}
|
|
@@ -483,13 +523,31 @@ typedef struct {
|
|
|
483
523
|
int aborted; /* set when push or match failed; the walk
|
|
484
524
|
* is short-circuited and the step driver
|
|
485
525
|
* propagates the error. */
|
|
526
|
+
/* The name test's prefix resolved once by eval_step (see have_pre), so a
|
|
527
|
+
* multi-node walk does not re-resolve it per node. */
|
|
528
|
+
const char *pre_uri;
|
|
529
|
+
size_t pre_uri_len;
|
|
530
|
+
int have_pre;
|
|
486
531
|
} step_visit_t;
|
|
487
532
|
|
|
488
533
|
static int
|
|
489
|
-
step_visit_cb(
|
|
534
|
+
step_visit_cb(MKR_DOM_NODE *n, void *u)
|
|
490
535
|
{
|
|
491
536
|
step_visit_t *st = (step_visit_t *)u;
|
|
492
|
-
|
|
537
|
+
/* Charge every visited node to the op budget. The axis walk is the dominant
|
|
538
|
+
* work of a step, and a low-selectivity walk visits (and name-tests) many
|
|
539
|
+
* nodes while pushing few or none - so without this the node-set cap (which
|
|
540
|
+
* only bounds the matched/pushed nodes) leaves the walk itself bounded only
|
|
541
|
+
* by document size, defeating max_eval_ops on a descendant walk that matches
|
|
542
|
+
* nothing. The index/first-match fast paths already tick per node; this
|
|
543
|
+
* brings the general walk to parity. On overrun we abort the walk exactly
|
|
544
|
+
* like a push failure. */
|
|
545
|
+
if (mkr_limit_eval_op(mkr_ctx_limits(st->ctx), st->err) != 0) {
|
|
546
|
+
st->aborted = 1;
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
549
|
+
if (node_principal_match_pre(&st->step->test, n, st->step->axis, st->ctx,
|
|
550
|
+
st->pre_uri, st->pre_uri_len, st->have_pre)) {
|
|
493
551
|
if (mkr_nodeset_push(st->out, n, mkr_ctx_limits(st->ctx), st->err) != 0) {
|
|
494
552
|
st->aborted = 1;
|
|
495
553
|
return 1; /* stop the walk; eval_step inspects st.aborted */
|
|
@@ -527,7 +585,7 @@ axis_can_alias(mkr_axis_t a)
|
|
|
527
585
|
}
|
|
528
586
|
|
|
529
587
|
/*
|
|
530
|
-
* 13 axes
|
|
588
|
+
* 13 axes - Phase 2b adds the remaining six. The namespace axis stays
|
|
531
589
|
* unimplemented for now (spec section 6 in mkr_xpath_internal.h).
|
|
532
590
|
*/
|
|
533
591
|
static int
|
|
@@ -590,7 +648,7 @@ is_reverse_axis(mkr_axis_t a)
|
|
|
590
648
|
}
|
|
591
649
|
|
|
592
650
|
/*
|
|
593
|
-
* Fast path for `//tag`
|
|
651
|
+
* Fast path for `//tag` - a descendant, unprefixed, predicate-free name-test
|
|
594
652
|
* whose context is exactly the document node. `descendant::tag` from the
|
|
595
653
|
* document is precisely "every element whose name is tag", which the
|
|
596
654
|
* document's element index already groups by tag id in document order, so we
|
|
@@ -600,12 +658,13 @@ is_reverse_axis(mkr_axis_t a)
|
|
|
600
658
|
* context does not qualify (caller falls back to the walk), -1 on error.
|
|
601
659
|
*
|
|
602
660
|
* Correctness: only taken for pure-HTML documents (mkr_element_index_has_foreign
|
|
603
|
-
* is false)
|
|
661
|
+
* is false) - in foreign content an element's qualified name need not equal its
|
|
604
662
|
* tag's canonical name, so a match could live in another tag bucket. Each
|
|
605
663
|
* candidate is still re-checked with node_principal_match, so the result is
|
|
606
664
|
* byte-identical to the walk (this also makes case/normalization quirks in the
|
|
607
665
|
* tag-name lookup harmless: a non-matching candidate is simply dropped).
|
|
608
666
|
*/
|
|
667
|
+
#ifndef MKR_HOST_XML
|
|
609
668
|
static int
|
|
610
669
|
try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
611
670
|
const mkr_nodeset_t *context_set, mkr_nodeset_t *result,
|
|
@@ -616,7 +675,7 @@ try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
616
675
|
|| step->test.prefix.ptr != NULL
|
|
617
676
|
|| step->test.local.ptr == NULL
|
|
618
677
|
|| context_set->count != 1
|
|
619
|
-
|| context_set->items[0] != (
|
|
678
|
+
|| context_set->items[0] != (MKR_DOM_NODE *)mkr_ctx_document(ctx)) {
|
|
620
679
|
return 0;
|
|
621
680
|
}
|
|
622
681
|
void *eidx = mkr_ctx_element_index(ctx);
|
|
@@ -625,13 +684,12 @@ try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
625
684
|
if (eidx == NULL || lookup == NULL || has_foreign == NULL || has_foreign(eidx)) {
|
|
626
685
|
return 0;
|
|
627
686
|
}
|
|
628
|
-
|
|
687
|
+
MKR_DOM_DOCUMENT *doc = mkr_ctx_document(ctx);
|
|
629
688
|
if (doc == NULL) {
|
|
630
689
|
return 0;
|
|
631
690
|
}
|
|
632
|
-
lxb_tag_id_t tag =
|
|
633
|
-
|
|
634
|
-
step->test.local.len);
|
|
691
|
+
lxb_tag_id_t tag = MKR_DOC_TAG_ID_BY_NAME(doc, step->test.local.ptr,
|
|
692
|
+
step->test.local.len);
|
|
635
693
|
/* The index covers only Lexbor's static tag-id range; a custom element's tag
|
|
636
694
|
* id is a (huge) pointer value and is not indexed. For such a name, fall back
|
|
637
695
|
* to the tree walk so those elements are still found. */
|
|
@@ -639,9 +697,13 @@ try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
639
697
|
return 0;
|
|
640
698
|
}
|
|
641
699
|
size_t cnt = 0;
|
|
642
|
-
|
|
700
|
+
/* The element index is HTML-only (the lookup hook is typed for lxb_dom); this
|
|
701
|
+
* whole fast path is unreachable for XML (its element index is NULL), but the
|
|
702
|
+
* cast keeps the shared body warning-clean under the XML instantiation. */
|
|
703
|
+
MKR_DOM_NODE *const *bucket = (MKR_DOM_NODE *const *)lookup(eidx, tag, &cnt);
|
|
643
704
|
for (size_t i = 0; i < cnt; ++i) {
|
|
644
|
-
|
|
705
|
+
if (mkr_limit_eval_op(mkr_ctx_limits(ctx), err) != 0) return -1;
|
|
706
|
+
MKR_DOM_NODE *n = bucket[i];
|
|
645
707
|
if (node_principal_match(&step->test, n, step->axis, ctx)) {
|
|
646
708
|
if (mkr_nodeset_push(result, n, mkr_ctx_limits(ctx), err) != 0) {
|
|
647
709
|
return -1;
|
|
@@ -650,6 +712,60 @@ try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
650
712
|
}
|
|
651
713
|
return 1;
|
|
652
714
|
}
|
|
715
|
+
#else /* MKR_HOST_XML */
|
|
716
|
+
/*
|
|
717
|
+
* XML analogue of the //tag fast path: a document-rooted, predicate-free
|
|
718
|
+
* descendant name test answered from the element-name index (mkr_xml_index.c)
|
|
719
|
+
* instead of walking. The index is keyed by (local name + namespace URI), so it
|
|
720
|
+
* serves a prefixed test (resolved URI) and an unprefixed strict test (the
|
|
721
|
+
* no-namespace bucket); an unprefixed LAX test means "any namespace", which a
|
|
722
|
+
* single bucket cannot express, so it falls back to the walk. The bucket already
|
|
723
|
+
* holds exactly the matching elements in document order, so they are pushed
|
|
724
|
+
* directly. Returns 1 (filled), 0 (does not qualify -> walk), -1 (error).
|
|
725
|
+
*/
|
|
726
|
+
static int
|
|
727
|
+
try_descendant_name_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
728
|
+
const mkr_nodeset_t *context_set, mkr_nodeset_t *result,
|
|
729
|
+
const char *pre_uri, size_t pre_uri_len, int have_pre,
|
|
730
|
+
mkr_xpath_error_t *err)
|
|
731
|
+
{
|
|
732
|
+
if (step->axis != MKR_AXIS_DESCENDANT
|
|
733
|
+
|| step->test.kind != MKR_NT_NAME
|
|
734
|
+
|| step->test.local.ptr == NULL
|
|
735
|
+
|| context_set->count != 1
|
|
736
|
+
|| context_set->items[0] != (MKR_DOM_NODE *)mkr_ctx_document(ctx)) {
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const char *ns_uri; size_t ns_uri_len;
|
|
741
|
+
if (step->test.prefix.ptr != NULL) {
|
|
742
|
+
if (!have_pre) return 0; /* should not happen: eval_step pre-resolves */
|
|
743
|
+
ns_uri = pre_uri; ns_uri_len = pre_uri_len;
|
|
744
|
+
} else if (mkr_ctx_unprefixed_lax(ctx)) {
|
|
745
|
+
return 0; /* "any namespace" - not a single bucket */
|
|
746
|
+
} else {
|
|
747
|
+
ns_uri = ""; ns_uri_len = 0; /* strict unprefixed -> no namespace */
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
void *owner = mkr_ctx_name_index_owner(ctx);
|
|
751
|
+
mkr_name_index_get_t get = mkr_ctx_name_index_get(ctx);
|
|
752
|
+
mkr_name_index_lookup_t lookup = mkr_ctx_name_index_lookup(ctx);
|
|
753
|
+
if (owner == NULL || get == NULL || lookup == NULL) return 0;
|
|
754
|
+
void *idx = get(owner); /* lazily builds + caches; NULL on OOM */
|
|
755
|
+
if (idx == NULL) return 0;
|
|
756
|
+
|
|
757
|
+
size_t cnt = 0;
|
|
758
|
+
void *const *bucket = lookup(idx, step->test.local.ptr, step->test.local.len,
|
|
759
|
+
ns_uri, ns_uri_len, &cnt);
|
|
760
|
+
for (size_t i = 0; i < cnt; ++i) {
|
|
761
|
+
if (mkr_limit_eval_op(mkr_ctx_limits(ctx), err) != 0) return -1;
|
|
762
|
+
if (mkr_nodeset_push(result, (MKR_DOM_NODE *)bucket[i], mkr_ctx_limits(ctx), err) != 0) {
|
|
763
|
+
return -1;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return 1;
|
|
767
|
+
}
|
|
768
|
+
#endif /* MKR_HOST_XML */
|
|
653
769
|
|
|
654
770
|
/* ---------------------------------------------------------------------------
|
|
655
771
|
* at_xpath() first-match short-circuit (Node#at_xpath / Node#at).
|
|
@@ -657,7 +773,7 @@ try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
657
773
|
* Node#at_xpath wants only the first node in document order; today it builds the
|
|
658
774
|
* whole node-set and takes [0]. For the common "find a descendant by name (plus
|
|
659
775
|
* a simple attribute predicate)" shapes we can instead walk the subtree in
|
|
660
|
-
* document order and stop at the first match
|
|
776
|
+
* document order and stop at the first match - the XPath-side analogue of
|
|
661
777
|
* at_css's lxb_selectors MATCH_FIRST.
|
|
662
778
|
*
|
|
663
779
|
* Recognised shapes (after the parser's // peephole, see mkr_apply_peephole):
|
|
@@ -668,7 +784,7 @@ try_descendant_tag_index(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
668
784
|
* position-independent [@name] / [@name='lit'] (exactly mkr_match_attr_pred's
|
|
669
785
|
* shape). Each denotes "the strict descendants of <start> matching the
|
|
670
786
|
* test+predicates, in document order", so the first node the pre-order walk
|
|
671
|
-
* reaches IS node-set[0] of the full evaluation
|
|
787
|
+
* reaches IS node-set[0] of the full evaluation - byte-identical, just without
|
|
672
788
|
* building the rest. Anything else (positional predicates, functions/variables,
|
|
673
789
|
* reverse axes, unions, prefixes, longer paths) returns 0 and the caller falls
|
|
674
790
|
* back to the full evaluator.
|
|
@@ -691,9 +807,11 @@ mkr_first_recognise(const mkr_node_t *ast, const mkr_step_t **out_step)
|
|
|
691
807
|
} else {
|
|
692
808
|
return 0;
|
|
693
809
|
}
|
|
694
|
-
/* A prefixed name test
|
|
695
|
-
*
|
|
696
|
-
|
|
810
|
+
/* A prefixed name test is allowed; mkr_try_first_match reproduces the step
|
|
811
|
+
* driver's "unknown prefix -> RUNTIME error" before walking, and
|
|
812
|
+
* node_principal_match resolves the prefix to a URI exactly as the full
|
|
813
|
+
* evaluator does. (Prefixed ATTRIBUTE predicates still fall back: their
|
|
814
|
+
* mkr_match_attr_step requires an unprefixed @name.) */
|
|
697
815
|
/* Every predicate must be a position-independent attribute predicate (no
|
|
698
816
|
* position()/last()/numeric, no function/variable -> no handler involvement). */
|
|
699
817
|
for (size_t p = 0; p < nt->npredicates; p++) {
|
|
@@ -705,26 +823,15 @@ mkr_first_recognise(const mkr_node_t *ast, const mkr_step_t **out_step)
|
|
|
705
823
|
}
|
|
706
824
|
|
|
707
825
|
/* Does +n+ satisfy every (already-recognised) attribute predicate of +step+?
|
|
708
|
-
*
|
|
826
|
+
* Uses the shared mkr_attr_pred_matches_node so it is identical to the
|
|
827
|
+
* predicate filter's per-node test by construction. */
|
|
709
828
|
static int
|
|
710
|
-
mkr_first_node_ok(const mkr_step_t *step,
|
|
829
|
+
mkr_first_node_ok(const mkr_step_t *step, MKR_DOM_NODE *n)
|
|
711
830
|
{
|
|
712
831
|
for (size_t p = 0; p < step->npredicates; p++) {
|
|
713
832
|
mkr_attr_pred_t ap;
|
|
714
833
|
(void)mkr_match_attr_pred(step->predicates[p], &ap); /* recogniser ensured it matches */
|
|
715
|
-
if (n
|
|
716
|
-
lxb_dom_attr_t *a =
|
|
717
|
-
mkr_attr_by_qualified_name(lxb_dom_interface_element(n), ap.name, ap.name_len);
|
|
718
|
-
if (a == NULL) return 0;
|
|
719
|
-
if (ap.has_value) {
|
|
720
|
-
size_t got_len = 0;
|
|
721
|
-
const lxb_char_t *got = lxb_dom_attr_value(a, &got_len);
|
|
722
|
-
if (got == NULL) got_len = 0;
|
|
723
|
-
if (got_len != ap.value_len
|
|
724
|
-
|| (ap.value_len != 0 && memcmp(got, ap.value, ap.value_len) != 0)) {
|
|
725
|
-
return 0;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
834
|
+
if (!mkr_attr_pred_matches_node(&ap, n)) return 0;
|
|
728
835
|
}
|
|
729
836
|
return 1;
|
|
730
837
|
}
|
|
@@ -732,31 +839,49 @@ mkr_first_node_ok(const mkr_step_t *step, lxb_dom_node_t *n)
|
|
|
732
839
|
/* If +ast+ is a recognised at_xpath() first-match shape, walk <start>'s strict
|
|
733
840
|
* descendants in document order and set *out_node to the first match (NULL if
|
|
734
841
|
* none). Returns 1 when handled (caller skips the full evaluator), 0 when the
|
|
735
|
-
* shape is not recognised
|
|
842
|
+
* shape is not recognised, and -1 when the per-evaluate op budget is exceeded
|
|
843
|
+
* during the walk (*err filled with MKR_XPATH_ERR_LIMIT). Every visited node is
|
|
844
|
+
* charged to max_eval_ops, so a huge late-/no-match document fails closed here
|
|
845
|
+
* exactly as it would in the full evaluator instead of bypassing the budget. */
|
|
736
846
|
int
|
|
737
847
|
mkr_try_first_match(mkr_xpath_context_t *ctx, const mkr_node_t *ast,
|
|
738
|
-
|
|
848
|
+
MKR_DOM_NODE **out_node, mkr_xpath_error_t *err)
|
|
739
849
|
{
|
|
740
850
|
const mkr_step_t *step;
|
|
741
851
|
if (out_node == NULL || !mkr_first_recognise(ast, &step)) return 0;
|
|
742
852
|
*out_node = NULL;
|
|
743
853
|
|
|
744
|
-
|
|
745
|
-
|
|
854
|
+
/* Reproduce the step driver's namespace-prefix validation: a prefixed name
|
|
855
|
+
* test with an unbound prefix is a RUNTIME error, not a silently empty match,
|
|
856
|
+
* so the fast path stays byte-identical to the full evaluator (incl. errors). */
|
|
857
|
+
if (step->test.prefix.ptr != NULL
|
|
858
|
+
&& mkr_ctx_lookup_ns(ctx, step->test.prefix.ptr, step->test.prefix.len, NULL) == NULL) {
|
|
859
|
+
mkr_err_setf(err, MKR_XPATH_ERR_RUNTIME,
|
|
860
|
+
"unknown namespace prefix '%s' in name test", step->test.prefix.ptr);
|
|
861
|
+
return -1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
MKR_DOM_NODE *start = ast->u.path.absolute
|
|
865
|
+
? (MKR_DOM_NODE *)mkr_ctx_document(ctx)
|
|
746
866
|
: mkr_ctx_node(ctx);
|
|
747
867
|
if (start == NULL) return 1; /* recognised; no context -> no match */
|
|
748
868
|
|
|
869
|
+
mkr_xpath_limits_t *limits = mkr_ctx_limits(ctx);
|
|
870
|
+
|
|
749
871
|
/* Iterative pre-order (document order) over STRICT descendants of start. */
|
|
750
|
-
for (
|
|
872
|
+
for (MKR_DOM_NODE *n = MKR_NODE_FIRST_CHILD(start); n != NULL; ) {
|
|
873
|
+
/* Charge each visited node to the per-evaluate op budget so this fast path
|
|
874
|
+
* is bounded by max_eval_ops just like the full evaluator (fail-closed). */
|
|
875
|
+
if (mkr_limit_eval_op(limits, err) != 0) return -1;
|
|
751
876
|
if (node_principal_match(&step->test, n, step->axis, ctx)
|
|
752
877
|
&& mkr_first_node_ok(step, n)) {
|
|
753
878
|
*out_node = n;
|
|
754
879
|
return 1;
|
|
755
880
|
}
|
|
756
|
-
if (n
|
|
757
|
-
while (n != start && n
|
|
881
|
+
if (MKR_NODE_FIRST_CHILD(n) != NULL) { n = MKR_NODE_FIRST_CHILD(n); continue; }
|
|
882
|
+
while (n != start && MKR_NODE_NEXT(n) == NULL) n = MKR_NODE_PARENT(n);
|
|
758
883
|
if (n == start) break;
|
|
759
|
-
n = n
|
|
884
|
+
n = MKR_NODE_NEXT(n);
|
|
760
885
|
}
|
|
761
886
|
return 1; /* recognised; *out_node is the first match or NULL */
|
|
762
887
|
}
|
|
@@ -772,15 +897,20 @@ eval_step(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
772
897
|
axis_name_str(step->axis));
|
|
773
898
|
return -1;
|
|
774
899
|
}
|
|
775
|
-
/*
|
|
776
|
-
* RUNTIME error instead of a silently empty match
|
|
777
|
-
*
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
900
|
+
/* Resolve the namespace prefix once up front (covers `prefix:local` and
|
|
901
|
+
* `prefix:*`): a uniform RUNTIME error instead of a silently empty match, and
|
|
902
|
+
* the resolved URI is then handed to every per-node match via step_visit_t so
|
|
903
|
+
* the walk does not re-resolve it per node. */
|
|
904
|
+
const char *pre_uri = NULL; size_t pre_uri_len = 0; int have_pre = 0;
|
|
905
|
+
if (step->test.prefix.ptr != NULL) {
|
|
906
|
+
pre_uri = mkr_ctx_lookup_ns(ctx, step->test.prefix.ptr, step->test.prefix.len, &pre_uri_len);
|
|
907
|
+
if (pre_uri == NULL) {
|
|
908
|
+
mkr_err_setf(err, MKR_XPATH_ERR_RUNTIME,
|
|
909
|
+
"unknown namespace prefix '%s' in name test",
|
|
910
|
+
step->test.prefix.ptr);
|
|
911
|
+
return -1;
|
|
912
|
+
}
|
|
913
|
+
have_pre = 1;
|
|
784
914
|
}
|
|
785
915
|
|
|
786
916
|
/* Post-pass = sort to doc order, then optional adjacent-dedup. We
|
|
@@ -792,7 +922,7 @@ eval_step(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
792
922
|
* 3. The axis does NOT aliase but multiple contexts can still
|
|
793
923
|
* produce results that interleave in doc order. child is the
|
|
794
924
|
* canonical case: html's children include head and body, and
|
|
795
|
-
* head's children include title
|
|
925
|
+
* head's children include title - concatenated naively gives
|
|
796
926
|
* [head, body, title, ...] but doc order is [head, title, body,
|
|
797
927
|
* ...]. Only SELF and ATTRIBUTE are safe to concatenate without
|
|
798
928
|
* sorting (their outputs stay in doc order when inputs are). */
|
|
@@ -806,9 +936,14 @@ eval_step(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
806
936
|
mkr_nodeset_init(&result);
|
|
807
937
|
|
|
808
938
|
if (step->npredicates == 0) {
|
|
809
|
-
/*
|
|
810
|
-
* tree walk entirely. */
|
|
939
|
+
/* A document-rooted descendant name test served from the element index,
|
|
940
|
+
* skipping the tree walk entirely (HTML: tag-id index; XML: name index). */
|
|
941
|
+
#ifdef MKR_HOST_XML
|
|
942
|
+
int fp = try_descendant_name_index(ctx, step, context_set, &result,
|
|
943
|
+
pre_uri, pre_uri_len, have_pre, err);
|
|
944
|
+
#else
|
|
811
945
|
int fp = try_descendant_tag_index(ctx, step, context_set, &result, err);
|
|
946
|
+
#endif
|
|
812
947
|
if (fp < 0) {
|
|
813
948
|
mkr_nodeset_clear(&result);
|
|
814
949
|
return -1;
|
|
@@ -820,7 +955,8 @@ eval_step(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
820
955
|
* path needs. Big win for //h3/parent::*, //h3/ancestor::div, and
|
|
821
956
|
* other multi-context reverse-axis sweeps where every context
|
|
822
957
|
* produces a single result. */
|
|
823
|
-
step_visit_t st = { .out = &result, .step = step, .ctx = ctx, .err = err, .aborted = 0
|
|
958
|
+
step_visit_t st = { .out = &result, .step = step, .ctx = ctx, .err = err, .aborted = 0,
|
|
959
|
+
.pre_uri = pre_uri, .pre_uri_len = pre_uri_len, .have_pre = have_pre };
|
|
824
960
|
for (size_t ci = 0; ci < context_set->count; ++ci) {
|
|
825
961
|
walk_axis(step->axis, context_set->items[ci], step_visit_cb, &st);
|
|
826
962
|
if (st.aborted) {
|
|
@@ -832,14 +968,15 @@ eval_step(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
832
968
|
} else {
|
|
833
969
|
/* Predicate path: position() / last() are per-context, so we have
|
|
834
970
|
* to materialise each context's fragment before filtering. Reuse
|
|
835
|
-
* a single fragment buffer across iterations
|
|
971
|
+
* a single fragment buffer across iterations - its items[] grows
|
|
836
972
|
* to the largest single-context cardinality once instead of
|
|
837
973
|
* malloc/freeing on every iteration. */
|
|
838
974
|
mkr_nodeset_t fragment;
|
|
839
975
|
mkr_nodeset_init(&fragment);
|
|
840
976
|
for (size_t ci = 0; ci < context_set->count; ++ci) {
|
|
841
977
|
fragment.count = 0;
|
|
842
|
-
step_visit_t st = { .out = &fragment, .step = step, .ctx = ctx, .err = err, .aborted = 0
|
|
978
|
+
step_visit_t st = { .out = &fragment, .step = step, .ctx = ctx, .err = err, .aborted = 0,
|
|
979
|
+
.pre_uri = pre_uri, .pre_uri_len = pre_uri_len, .have_pre = have_pre };
|
|
843
980
|
walk_axis(step->axis, context_set->items[ci], step_visit_cb, &st);
|
|
844
981
|
if (st.aborted) {
|
|
845
982
|
mkr_nodeset_clear(&fragment);
|
|
@@ -883,13 +1020,175 @@ eval_step(mkr_xpath_context_t *ctx, const mkr_step_t *step,
|
|
|
883
1020
|
return 0;
|
|
884
1021
|
}
|
|
885
1022
|
|
|
1023
|
+
/* --- `//name[N]` index fast path (shared shape + per-parent emit) -----------
|
|
1024
|
+
*
|
|
1025
|
+
* `//name[N]` (the two leading steps `descendant-or-self::node()` /
|
|
1026
|
+
* `child::name[N]`, N a positive integer literal, rooted at the document)
|
|
1027
|
+
* selects, for every node c, c's Nth name-child - i.e. every name element that
|
|
1028
|
+
* is the Nth name-child of its parent. This is NOT `(//name)[N]` nor
|
|
1029
|
+
* `descendant::name[N]`. Both hosts answer it from their element index (XML: the
|
|
1030
|
+
* name index; HTML: the tag-id index) with per-parent counting, instead of the
|
|
1031
|
+
* descendant-or-self full-tree walk: the index lists matching elements in
|
|
1032
|
+
* document order, so a parent's name-children appear among them in child order;
|
|
1033
|
+
* one sweep with a pointer-keyed parent->count map emits exactly those whose
|
|
1034
|
+
* running count reaches N, already in document order (no sort/dedup). The bucket
|
|
1035
|
+
* retrieval is host-specific; the shape check and the count loop are shared. */
|
|
1036
|
+
|
|
1037
|
+
/* Host-neutral shape check: sets *need = N. 1 = matches, 0 = no. */
|
|
1038
|
+
static int
|
|
1039
|
+
nth_shape(mkr_xpath_context_t *ctx, const mkr_step_t *s0, const mkr_step_t *s1,
|
|
1040
|
+
const mkr_nodeset_t *seed, size_t *need)
|
|
1041
|
+
{
|
|
1042
|
+
if (s0->axis != MKR_AXIS_DESCENDANT_OR_SELF || s0->test.kind != MKR_NT_NODE
|
|
1043
|
+
|| s0->test.prefix.ptr != NULL || s0->npredicates != 0)
|
|
1044
|
+
return 0;
|
|
1045
|
+
if (s1->axis != MKR_AXIS_CHILD || s1->test.kind != MKR_NT_NAME
|
|
1046
|
+
|| s1->test.local.ptr == NULL || s1->npredicates != 1)
|
|
1047
|
+
return 0;
|
|
1048
|
+
/* The sole predicate must be a bare positive-integer literal `[N]` (= position()
|
|
1049
|
+
* == N). `[position()=N]`, `[last()]`, etc. are binops/calls -> fall back. */
|
|
1050
|
+
const mkr_node_t *pred = s1->predicates[0];
|
|
1051
|
+
if (pred == NULL || pred->kind != MKR_NK_LITERAL_NUM) return 0;
|
|
1052
|
+
double dn = pred->u.literal_num;
|
|
1053
|
+
if (!(dn >= 1.0) || dn != (double)(size_t)dn) return 0;
|
|
1054
|
+
/* Context must be exactly the document node (the index's scope). */
|
|
1055
|
+
if (seed->count != 1 || seed->items[0] != (MKR_DOM_NODE *)mkr_ctx_document(ctx))
|
|
1056
|
+
return 0;
|
|
1057
|
+
*need = (size_t)dn;
|
|
1058
|
+
return 1;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/* Emit, in document order, each bucket element that is the +need+th of its
|
|
1062
|
+
* parent. When +test+ != NULL only elements matching it count (the HTML tag
|
|
1063
|
+
* bucket may include tag-id-quirk non-matches; node_principal_match re-checks).
|
|
1064
|
+
* The op budget is charged per element. Returns 0, -1 (OOM/limit), or -2
|
|
1065
|
+
* (cap-sizer overflow -> caller falls back to the generic evaluator). */
|
|
1066
|
+
static int
|
|
1067
|
+
emit_nth_per_parent(mkr_xpath_context_t *ctx, MKR_DOM_NODE *const *bucket, size_t cnt,
|
|
1068
|
+
size_t need, const mkr_nodetest_t *test, mkr_axis_t axis,
|
|
1069
|
+
mkr_nodeset_t *result, mkr_xpath_error_t *err)
|
|
1070
|
+
{
|
|
1071
|
+
if (cnt == 0) return 0;
|
|
1072
|
+
size_t cap;
|
|
1073
|
+
if (!mkr_pow2_ceil(cnt + (cnt >> 1) + 1, &cap)) return -2;
|
|
1074
|
+
struct nth_slot { const void *parent; size_t count; } *tab =
|
|
1075
|
+
mkr_callocarray(cap, sizeof(*tab));
|
|
1076
|
+
if (tab == NULL) { mkr_err_set(err, MKR_XPATH_ERR_OOM, "out of memory (//name[N])"); return -1; }
|
|
1077
|
+
const size_t mask = cap - 1;
|
|
1078
|
+
mkr_xpath_limits_t *limits = mkr_ctx_limits(ctx);
|
|
1079
|
+
int rc = 0;
|
|
1080
|
+
for (size_t i = 0; i < cnt; ++i) {
|
|
1081
|
+
if (mkr_limit_eval_op(limits, err) != 0) { rc = -1; break; } /* same budget as the walk */
|
|
1082
|
+
MKR_DOM_NODE *e = bucket[i];
|
|
1083
|
+
if (test != NULL && !node_principal_match(test, e, axis, ctx)) continue;
|
|
1084
|
+
const void *par = (const void *)MKR_NODE_PARENT(e);
|
|
1085
|
+
size_t h = (size_t)mkr_ptr_hash(par) & mask;
|
|
1086
|
+
while (tab[h].parent != NULL && tab[h].parent != par) h = (h + 1) & mask;
|
|
1087
|
+
if (tab[h].parent == NULL) { tab[h].parent = par; tab[h].count = 0; }
|
|
1088
|
+
if (++tab[h].count == need) { /* e is the Nth name-child of its parent */
|
|
1089
|
+
if (mkr_nodeset_push(result, e, limits, err) != 0) { rc = -1; break; }
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
free(tab);
|
|
1093
|
+
return rc;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/* Returns 1 (filled *result; the caller consumes the two leading steps),
|
|
1097
|
+
* 0 (not eligible -> caller evaluates normally), -1 (error). */
|
|
1098
|
+
#ifdef MKR_HOST_XML
|
|
1099
|
+
static int
|
|
1100
|
+
try_descendant_index_nth(mkr_xpath_context_t *ctx,
|
|
1101
|
+
const mkr_step_t *s0, const mkr_step_t *s1,
|
|
1102
|
+
const mkr_nodeset_t *seed,
|
|
1103
|
+
mkr_nodeset_t *result, mkr_xpath_error_t *err)
|
|
1104
|
+
{
|
|
1105
|
+
size_t need;
|
|
1106
|
+
if (!nth_shape(ctx, s0, s1, seed, &need)) return 0;
|
|
1107
|
+
|
|
1108
|
+
/* Resolve the name test's namespace, mirroring try_descendant_name_index. */
|
|
1109
|
+
const char *ns_uri; size_t ns_uri_len;
|
|
1110
|
+
if (s1->test.prefix.ptr != NULL) {
|
|
1111
|
+
ns_uri = mkr_ctx_lookup_ns(ctx, s1->test.prefix.ptr, s1->test.prefix.len, &ns_uri_len);
|
|
1112
|
+
if (ns_uri == NULL) {
|
|
1113
|
+
mkr_err_setf(err, MKR_XPATH_ERR_RUNTIME,
|
|
1114
|
+
"unknown namespace prefix '%s' in name test", s1->test.prefix.ptr);
|
|
1115
|
+
return -1;
|
|
1116
|
+
}
|
|
1117
|
+
} else if (mkr_ctx_unprefixed_lax(ctx)) {
|
|
1118
|
+
return 0; /* any namespace -> not a single bucket */
|
|
1119
|
+
} else {
|
|
1120
|
+
ns_uri = ""; ns_uri_len = 0; /* strict unprefixed -> no namespace */
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
void *owner = mkr_ctx_name_index_owner(ctx);
|
|
1124
|
+
mkr_name_index_get_t get = mkr_ctx_name_index_get(ctx);
|
|
1125
|
+
mkr_name_index_lookup_t lookup = mkr_ctx_name_index_lookup(ctx);
|
|
1126
|
+
if (owner == NULL || get == NULL || lookup == NULL) return 0;
|
|
1127
|
+
void *idx = get(owner);
|
|
1128
|
+
if (idx == NULL) return 0;
|
|
1129
|
+
|
|
1130
|
+
size_t cnt = 0;
|
|
1131
|
+
void *const *bucket = lookup(idx, s1->test.local.ptr, s1->test.local.len,
|
|
1132
|
+
ns_uri, ns_uri_len, &cnt);
|
|
1133
|
+
/* The name-index bucket already holds exactly the matches, so no re-check. */
|
|
1134
|
+
int rc = emit_nth_per_parent(ctx, (MKR_DOM_NODE *const *)bucket, cnt, need,
|
|
1135
|
+
NULL, s1->axis, result, err);
|
|
1136
|
+
return rc == -2 ? 0 : (rc < 0 ? -1 : 1);
|
|
1137
|
+
}
|
|
1138
|
+
#else /* HTML: tag-id index analogue (unprefixed, pure-HTML, static tag range) */
|
|
1139
|
+
static int
|
|
1140
|
+
try_descendant_index_nth(mkr_xpath_context_t *ctx,
|
|
1141
|
+
const mkr_step_t *s0, const mkr_step_t *s1,
|
|
1142
|
+
const mkr_nodeset_t *seed,
|
|
1143
|
+
mkr_nodeset_t *result, mkr_xpath_error_t *err)
|
|
1144
|
+
{
|
|
1145
|
+
size_t need;
|
|
1146
|
+
if (!nth_shape(ctx, s0, s1, seed, &need)) return 0;
|
|
1147
|
+
if (s1->test.prefix.ptr != NULL) return 0; /* the tag index is unprefixed-only */
|
|
1148
|
+
|
|
1149
|
+
void *eidx = mkr_ctx_element_index(ctx);
|
|
1150
|
+
mkr_tag_index_lookup_t lookup = mkr_ctx_tag_lookup(ctx);
|
|
1151
|
+
mkr_tag_index_foreign_t has_foreign = mkr_ctx_tag_has_foreign(ctx);
|
|
1152
|
+
if (eidx == NULL || lookup == NULL || has_foreign == NULL || has_foreign(eidx)) return 0;
|
|
1153
|
+
MKR_DOM_DOCUMENT *doc = mkr_ctx_document(ctx);
|
|
1154
|
+
if (doc == NULL) return 0;
|
|
1155
|
+
lxb_tag_id_t tag = MKR_DOC_TAG_ID_BY_NAME(doc, s1->test.local.ptr, s1->test.local.len);
|
|
1156
|
+
if (tag == LXB_TAG__UNDEF || (uintptr_t)tag >= LXB_TAG__LAST_ENTRY) return 0;
|
|
1157
|
+
|
|
1158
|
+
size_t cnt = 0;
|
|
1159
|
+
MKR_DOM_NODE *const *bucket = (MKR_DOM_NODE *const *)lookup(eidx, tag, &cnt);
|
|
1160
|
+
/* Re-check each candidate (tag-name lookup can have case/normalization quirks),
|
|
1161
|
+
* so the per-parent count is over the actual matches - byte-identical to the
|
|
1162
|
+
* walk, exactly as try_descendant_tag_index does for the no-predicate case. */
|
|
1163
|
+
int rc = emit_nth_per_parent(ctx, bucket, cnt, need, &s1->test, s1->axis, result, err);
|
|
1164
|
+
return rc == -2 ? 0 : (rc < 0 ? -1 : 1);
|
|
1165
|
+
}
|
|
1166
|
+
#endif
|
|
1167
|
+
|
|
886
1168
|
static int
|
|
887
1169
|
eval_steps(mkr_xpath_context_t *ctx, mkr_step_t *steps, size_t nsteps,
|
|
888
1170
|
mkr_nodeset_t *seed, mkr_val_t *out, mkr_xpath_error_t *err)
|
|
889
1171
|
{
|
|
890
1172
|
mkr_nodeset_t current = *seed; /* take ownership */
|
|
891
1173
|
memset(seed, 0, sizeof(*seed));
|
|
892
|
-
|
|
1174
|
+
size_t s_start = 0;
|
|
1175
|
+
/* `//name[N]` (descendant-or-self::node()/child::name[N]) from the document:
|
|
1176
|
+
* serve the two leading steps from the element index (XML: name index; HTML:
|
|
1177
|
+
* tag-id index) with per-parent counting instead of the full-tree walk. */
|
|
1178
|
+
if (nsteps >= 2) {
|
|
1179
|
+
mkr_nodeset_t nth;
|
|
1180
|
+
mkr_nodeset_init(&nth);
|
|
1181
|
+
int fp = try_descendant_index_nth(ctx, &steps[0], &steps[1], ¤t, &nth, err);
|
|
1182
|
+
if (fp < 0) { mkr_nodeset_clear(&nth); mkr_nodeset_clear(¤t); return -1; }
|
|
1183
|
+
if (fp == 1) {
|
|
1184
|
+
mkr_nodeset_clear(¤t);
|
|
1185
|
+
current = nth; /* result of //name[N]; continue from the 3rd step */
|
|
1186
|
+
s_start = 2;
|
|
1187
|
+
} else {
|
|
1188
|
+
mkr_nodeset_clear(&nth);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
for (size_t s = s_start; s < nsteps; ++s) {
|
|
893
1192
|
mkr_nodeset_t next;
|
|
894
1193
|
mkr_nodeset_init(&next);
|
|
895
1194
|
if (eval_step(ctx, &steps[s], ¤t, &next, err) != 0) {
|
|
@@ -923,13 +1222,17 @@ compare_eq(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
923
1222
|
/* All node string-values go through the per-eval cache. For an
|
|
924
1223
|
* M-by-N nodeset comparison this turns O(M*N) computations into
|
|
925
1224
|
* at most O(M+N). */
|
|
926
|
-
(
|
|
1225
|
+
/* O(M*N) pair scan: string-value CONSTRUCTION is O(M+N) via the cache, but
|
|
1226
|
+
* the comparison loop itself is M*N, so charge each pair to the op budget -
|
|
1227
|
+
* otherwise an all-pairs node-set equality drives up to ~1e14 memcmps as a
|
|
1228
|
+
* handful of ops. */
|
|
927
1229
|
for (size_t i = 0; i < l->u.nodeset.count; ++i) {
|
|
928
1230
|
mkr_borrowed_text_t ls;
|
|
929
1231
|
if (mkr_get_cached_node_text(ctx, l->u.nodeset.items[i], &ls, err) != 0) {
|
|
930
1232
|
return -1;
|
|
931
1233
|
}
|
|
932
1234
|
for (size_t j = 0; j < r->u.nodeset.count; ++j) {
|
|
1235
|
+
if (mkr_limit_eval_op(L, err) != 0) return -1;
|
|
933
1236
|
mkr_borrowed_text_t rs;
|
|
934
1237
|
if (mkr_get_cached_node_text(ctx, r->u.nodeset.items[j], &rs, err) != 0) {
|
|
935
1238
|
return -1;
|
|
@@ -949,6 +1252,7 @@ compare_eq(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
949
1252
|
if (sc->type == MKR_XPATH_TYPE_NUMBER) {
|
|
950
1253
|
double target = sc->u.number;
|
|
951
1254
|
for (size_t i = 0; i < ns->u.nodeset.count; ++i) {
|
|
1255
|
+
if (mkr_limit_eval_op(L, err) != 0) return -1;
|
|
952
1256
|
mkr_borrowed_text_t s;
|
|
953
1257
|
if (mkr_get_cached_node_text(ctx, ns->u.nodeset.items[i], &s, err) != 0) {
|
|
954
1258
|
return -1;
|
|
@@ -969,6 +1273,7 @@ compare_eq(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
969
1273
|
mkr_owned_text_t target;
|
|
970
1274
|
if (mkr_val_to_owned_text_or_fail(sc, L, err, &target) != 0) return -1;
|
|
971
1275
|
for (size_t i = 0; i < ns->u.nodeset.count; ++i) {
|
|
1276
|
+
if (mkr_limit_eval_op(L, err) != 0) { mkr_owned_text_clear(&target); return -1; }
|
|
972
1277
|
mkr_borrowed_text_t s;
|
|
973
1278
|
if (mkr_get_cached_node_text(ctx, ns->u.nodeset.items[i], &s, err) != 0) {
|
|
974
1279
|
mkr_owned_text_clear(&target); return -1;
|
|
@@ -989,7 +1294,7 @@ compare_eq(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
989
1294
|
}
|
|
990
1295
|
if (l->type == MKR_XPATH_TYPE_NUMBER || r->type == MKR_XPATH_TYPE_NUMBER) {
|
|
991
1296
|
/* Both operands are non-nodeset (the nodeset case is handled
|
|
992
|
-
* above); _unchecked is the right entry
|
|
1297
|
+
* above); _unchecked is the right entry - no allocation possible. */
|
|
993
1298
|
double a = mkr_val_to_number_unchecked(l);
|
|
994
1299
|
double b = mkr_val_to_number_unchecked(r);
|
|
995
1300
|
int eq = (a == b);
|
|
@@ -1025,11 +1330,13 @@ compare_rel(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
1025
1330
|
{
|
|
1026
1331
|
mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
|
|
1027
1332
|
|
|
1028
|
-
/* node-set vs node-set (XPath 1.0 §3.4): true iff SOME pair of nodes
|
|
1029
|
-
* from each set
|
|
1333
|
+
/* node-set vs node-set (XPath 1.0 §3.4): true iff SOME pair of nodes - one
|
|
1334
|
+
* from each set - satisfies the relation on their numeric string-values.
|
|
1030
1335
|
* Must compare every pair, not just the first node of each side. */
|
|
1031
1336
|
if (l->type == MKR_XPATH_TYPE_NODESET && r->type == MKR_XPATH_TYPE_NODESET) {
|
|
1032
|
-
(
|
|
1337
|
+
/* O(M*N) pair scan (see compare_eq): charge each pair to the op budget so
|
|
1338
|
+
* an all-pairs relational compare cannot drive M*N number conversions as a
|
|
1339
|
+
* handful of ops. */
|
|
1033
1340
|
for (size_t i = 0; i < l->u.nodeset.count; ++i) {
|
|
1034
1341
|
mkr_borrowed_text_t ls;
|
|
1035
1342
|
if (mkr_get_cached_node_text(ctx, l->u.nodeset.items[i], &ls, err) != 0) {
|
|
@@ -1037,6 +1344,7 @@ compare_rel(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
1037
1344
|
}
|
|
1038
1345
|
double a = mkr_borrowed_text_to_number(ls);
|
|
1039
1346
|
for (size_t j = 0; j < r->u.nodeset.count; ++j) {
|
|
1347
|
+
if (mkr_limit_eval_op(L, err) != 0) return -1;
|
|
1040
1348
|
mkr_borrowed_text_t rs;
|
|
1041
1349
|
if (mkr_get_cached_node_text(ctx, r->u.nodeset.items[j], &rs, err) != 0) {
|
|
1042
1350
|
return -1;
|
|
@@ -1058,6 +1366,7 @@ compare_rel(mkr_xpath_context_t *ctx, const mkr_val_t *l, const mkr_val_t *r,
|
|
|
1058
1366
|
double scn;
|
|
1059
1367
|
if (mkr_val_to_number_or_fail(sc, L, err, &scn) != 0) return -1;
|
|
1060
1368
|
for (size_t i = 0; i < ns->u.nodeset.count; ++i) {
|
|
1369
|
+
if (mkr_limit_eval_op(L, err) != 0) return -1;
|
|
1061
1370
|
mkr_borrowed_text_t s;
|
|
1062
1371
|
if (mkr_get_cached_node_text(ctx, ns->u.nodeset.items[i], &s, err) != 0) {
|
|
1063
1372
|
return -1;
|
|
@@ -1095,7 +1404,7 @@ union_nodeset(mkr_xpath_context_t *ctx, mkr_val_t *l, mkr_val_t *r, mkr_val_t *o
|
|
|
1095
1404
|
}
|
|
1096
1405
|
/* Push both sides without per-insert deduplication (the old contains()
|
|
1097
1406
|
* loop was O(n^2)). Sort once at the end and collapse adjacent
|
|
1098
|
-
* duplicates
|
|
1407
|
+
* duplicates - total cost is O(n log n) plus a single linear pass. */
|
|
1099
1408
|
mkr_nodeset_t merged;
|
|
1100
1409
|
mkr_nodeset_init(&merged);
|
|
1101
1410
|
for (size_t i = 0; i < l->u.nodeset.count; ++i) {
|
|
@@ -1122,13 +1431,13 @@ union_nodeset(mkr_xpath_context_t *ctx, mkr_val_t *l, mkr_val_t *r, mkr_val_t *o
|
|
|
1122
1431
|
|
|
1123
1432
|
static int
|
|
1124
1433
|
eval_path(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
1125
|
-
|
|
1434
|
+
MKR_DOM_NODE *self_node,
|
|
1126
1435
|
mkr_val_t *out, mkr_xpath_error_t *err)
|
|
1127
1436
|
{
|
|
1128
1437
|
mkr_nodeset_t seed;
|
|
1129
1438
|
mkr_nodeset_init(&seed);
|
|
1130
1439
|
if (n->u.path.absolute) {
|
|
1131
|
-
|
|
1440
|
+
MKR_DOM_NODE *root = (MKR_DOM_NODE *)mkr_ctx_document(ctx);
|
|
1132
1441
|
if (root == NULL) {
|
|
1133
1442
|
mkr_err_set(err, MKR_XPATH_ERR_RUNTIME, "absolute path with no document");
|
|
1134
1443
|
return -1;
|
|
@@ -1148,11 +1457,11 @@ eval_path(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1148
1457
|
|
|
1149
1458
|
static int
|
|
1150
1459
|
eval_filter(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
1151
|
-
|
|
1460
|
+
const mkr_focus_t *focus,
|
|
1152
1461
|
mkr_val_t *out, mkr_xpath_error_t *err)
|
|
1153
1462
|
{
|
|
1154
1463
|
mkr_val_t primary = {0};
|
|
1155
|
-
if (eval_node(ctx, n->u.filter.expr,
|
|
1464
|
+
if (eval_node(ctx, n->u.filter.expr, focus, &primary, err) != 0) return -1;
|
|
1156
1465
|
if (n->u.filter.npreds > 0) {
|
|
1157
1466
|
if (primary.type != MKR_XPATH_TYPE_NODESET) {
|
|
1158
1467
|
mkr_val_clear(&primary);
|
|
@@ -1181,11 +1490,12 @@ eval_filter(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1181
1490
|
|
|
1182
1491
|
/* ---------- function library bridge ---------- */
|
|
1183
1492
|
|
|
1184
|
-
|
|
1493
|
+
/* mkr_lookup_function is the file-static function table, defined earlier in this
|
|
1494
|
+
* merged engine TU (mkr_xpath_funcs_body.h, included before this body). */
|
|
1185
1495
|
|
|
1186
1496
|
static int
|
|
1187
1497
|
eval_fncall(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
1188
|
-
|
|
1498
|
+
const mkr_focus_t *focus,
|
|
1189
1499
|
mkr_val_t *out, mkr_xpath_error_t *err)
|
|
1190
1500
|
{
|
|
1191
1501
|
const char *ns_uri = NULL;
|
|
@@ -1208,7 +1518,7 @@ eval_fncall(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1208
1518
|
return -1;
|
|
1209
1519
|
}
|
|
1210
1520
|
for (size_t i = 0; i < nargs; ++i) {
|
|
1211
|
-
if (eval_node(ctx, n->u.fncall.args[i],
|
|
1521
|
+
if (eval_node(ctx, n->u.fncall.args[i], focus, &args[i], err) != 0) {
|
|
1212
1522
|
for (size_t j = 0; j <= i; ++j) mkr_val_clear(&args[j]);
|
|
1213
1523
|
free(args);
|
|
1214
1524
|
return -1;
|
|
@@ -1218,7 +1528,7 @@ eval_fncall(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1218
1528
|
|
|
1219
1529
|
int rc;
|
|
1220
1530
|
if (fn != NULL) {
|
|
1221
|
-
rc = fn(ctx,
|
|
1531
|
+
rc = fn(ctx, focus, args, nargs, out, err);
|
|
1222
1532
|
} else {
|
|
1223
1533
|
/* No built-in match. Delegate to the per-call resolver (the Ruby
|
|
1224
1534
|
* handler bridge installs one for the duration of evaluate()). */
|
|
@@ -1226,7 +1536,7 @@ eval_fncall(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1226
1536
|
int resolved = 1; /* not found by default */
|
|
1227
1537
|
if (resolver != NULL) {
|
|
1228
1538
|
resolved = resolver(mkr_xpath_get_user_data(ctx),
|
|
1229
|
-
ctx, (void *)
|
|
1539
|
+
ctx, (void *)focus->node, focus->pos, focus->size,
|
|
1230
1540
|
ns_uri, n->u.fncall.name.ptr,
|
|
1231
1541
|
(void *)args, nargs, (void *)out, err);
|
|
1232
1542
|
}
|
|
@@ -1248,13 +1558,13 @@ eval_fncall(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1248
1558
|
|
|
1249
1559
|
static int
|
|
1250
1560
|
eval_binop(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
1251
|
-
|
|
1561
|
+
const mkr_focus_t *focus,
|
|
1252
1562
|
mkr_val_t *out, mkr_xpath_error_t *err)
|
|
1253
1563
|
{
|
|
1254
1564
|
/* AND/OR short-circuit. */
|
|
1255
1565
|
if (n->u.binop.op == MKR_OP_OR || n->u.binop.op == MKR_OP_AND) {
|
|
1256
1566
|
mkr_val_t l = {0};
|
|
1257
|
-
if (eval_node(ctx, n->u.binop.lhs,
|
|
1567
|
+
if (eval_node(ctx, n->u.binop.lhs, focus, &l, err) != 0) return -1;
|
|
1258
1568
|
int lb = mkr_val_to_boolean(&l);
|
|
1259
1569
|
mkr_val_clear(&l);
|
|
1260
1570
|
if (n->u.binop.op == MKR_OP_OR && lb) {
|
|
@@ -1264,7 +1574,7 @@ eval_binop(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1264
1574
|
out->type = MKR_XPATH_TYPE_BOOLEAN; out->u.boolean = 0; return 0;
|
|
1265
1575
|
}
|
|
1266
1576
|
mkr_val_t r = {0};
|
|
1267
|
-
if (eval_node(ctx, n->u.binop.rhs,
|
|
1577
|
+
if (eval_node(ctx, n->u.binop.rhs, focus, &r, err) != 0) return -1;
|
|
1268
1578
|
int rb = mkr_val_to_boolean(&r);
|
|
1269
1579
|
mkr_val_clear(&r);
|
|
1270
1580
|
out->type = MKR_XPATH_TYPE_BOOLEAN; out->u.boolean = rb;
|
|
@@ -1272,8 +1582,8 @@ eval_binop(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1272
1582
|
}
|
|
1273
1583
|
|
|
1274
1584
|
mkr_val_t l = {0}, r = {0};
|
|
1275
|
-
if (eval_node(ctx, n->u.binop.lhs,
|
|
1276
|
-
if (eval_node(ctx, n->u.binop.rhs,
|
|
1585
|
+
if (eval_node(ctx, n->u.binop.lhs, focus, &l, err) != 0) return -1;
|
|
1586
|
+
if (eval_node(ctx, n->u.binop.rhs, focus, &r, err) != 0) {
|
|
1277
1587
|
mkr_val_clear(&l); return -1;
|
|
1278
1588
|
}
|
|
1279
1589
|
int rc = 0;
|
|
@@ -1342,7 +1652,7 @@ eval_binop(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1342
1652
|
|
|
1343
1653
|
static int
|
|
1344
1654
|
eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
1345
|
-
|
|
1655
|
+
const mkr_focus_t *focus,
|
|
1346
1656
|
mkr_val_t *out, mkr_xpath_error_t *err)
|
|
1347
1657
|
{
|
|
1348
1658
|
/* Budget + recursion bookkeeping. Every AST node visit counts as one
|
|
@@ -1354,7 +1664,7 @@ eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1354
1664
|
|
|
1355
1665
|
/* Hoisting fast path: a CI subtree that's already been computed in
|
|
1356
1666
|
* this evaluate is returned as a clone. The clone keeps ownership
|
|
1357
|
-
* semantics clean
|
|
1667
|
+
* semantics clean - mkr_val_clear on either copy is safe. */
|
|
1358
1668
|
if (n->is_context_independent && n->memoized) {
|
|
1359
1669
|
int rc = mkr_val_clone(&n->memo_value, out, err);
|
|
1360
1670
|
mkr_limit_recurse_leave(L);
|
|
@@ -1399,11 +1709,11 @@ eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1399
1709
|
break;
|
|
1400
1710
|
}
|
|
1401
1711
|
case MKR_NK_FNCALL:
|
|
1402
|
-
rc = eval_fncall(ctx, n,
|
|
1712
|
+
rc = eval_fncall(ctx, n, focus, out, err);
|
|
1403
1713
|
break;
|
|
1404
1714
|
case MKR_NK_UNARY: {
|
|
1405
1715
|
mkr_val_t v = {0};
|
|
1406
|
-
if (eval_node(ctx, n->u.unary.expr,
|
|
1716
|
+
if (eval_node(ctx, n->u.unary.expr, focus, &v, err) != 0) {
|
|
1407
1717
|
rc = -1;
|
|
1408
1718
|
break;
|
|
1409
1719
|
}
|
|
@@ -1420,13 +1730,13 @@ eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1420
1730
|
break;
|
|
1421
1731
|
}
|
|
1422
1732
|
case MKR_NK_BINOP:
|
|
1423
|
-
rc = eval_binop(ctx, n,
|
|
1733
|
+
rc = eval_binop(ctx, n, focus, out, err);
|
|
1424
1734
|
break;
|
|
1425
1735
|
case MKR_NK_PATH:
|
|
1426
|
-
rc = eval_path(ctx, n,
|
|
1736
|
+
rc = eval_path(ctx, n, focus->node, out, err);
|
|
1427
1737
|
break;
|
|
1428
1738
|
case MKR_NK_FILTER:
|
|
1429
|
-
rc = eval_filter(ctx, n,
|
|
1739
|
+
rc = eval_filter(ctx, n, focus, out, err);
|
|
1430
1740
|
break;
|
|
1431
1741
|
default:
|
|
1432
1742
|
mkr_err_set(err, MKR_XPATH_ERR_INTERNAL, "unknown AST node");
|
|
@@ -1435,7 +1745,7 @@ eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1435
1745
|
}
|
|
1436
1746
|
|
|
1437
1747
|
/* Memoize on success when the subtree is CI. The clone keeps the
|
|
1438
|
-
* caller's out value independent of the cached one
|
|
1748
|
+
* caller's out value independent of the cached one - important
|
|
1439
1749
|
* because the caller is free to consume/modify their copy. */
|
|
1440
1750
|
if (rc == 0 && n->is_context_independent && !n->memoized) {
|
|
1441
1751
|
mkr_val_t memo = {0};
|
|
@@ -1446,7 +1756,7 @@ eval_node(mkr_xpath_context_t *ctx, const mkr_node_t *n,
|
|
|
1446
1756
|
mut->memo_value = memo;
|
|
1447
1757
|
mut->memoized = 1;
|
|
1448
1758
|
} else {
|
|
1449
|
-
/* OOM during clone
|
|
1759
|
+
/* OOM during clone - the caller's `out` is still valid. We
|
|
1450
1760
|
* leave the node unmemoized and surface the error. */
|
|
1451
1761
|
rc = -1;
|
|
1452
1762
|
}
|
|
@@ -1460,6 +1770,6 @@ int
|
|
|
1460
1770
|
mkr_eval_ast(mkr_xpath_context_t *ctx, const mkr_node_t *ast,
|
|
1461
1771
|
mkr_val_t *out, mkr_xpath_error_t *err)
|
|
1462
1772
|
{
|
|
1463
|
-
|
|
1464
|
-
return eval_node(ctx, ast,
|
|
1773
|
+
mkr_focus_t focus = { mkr_ctx_node(ctx), 1, 1 };
|
|
1774
|
+
return eval_node(ctx, ast, &focus, out, err);
|
|
1465
1775
|
}
|