makiri 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +22 -0
  3. data/.github/workflows/libfuzzer.yml +83 -0
  4. data/.github/workflows/release.yml +12 -7
  5. data/.github/workflows/security.yml +88 -3
  6. data/.github/workflows/valgrind.yml +135 -0
  7. data/CHANGELOG.md +152 -15
  8. data/README.md +183 -13
  9. data/Rakefile +294 -7
  10. data/ext/makiri/bridge/bridge.h +28 -0
  11. data/ext/makiri/bridge/ruby_string.c +282 -12
  12. data/ext/makiri/core/mkr_alloc.c +40 -3
  13. data/ext/makiri/core/mkr_alloc.h +28 -5
  14. data/ext/makiri/core/mkr_buf.c +47 -3
  15. data/ext/makiri/core/mkr_buf.h +112 -3
  16. data/ext/makiri/core/mkr_core.c +143 -0
  17. data/ext/makiri/core/mkr_core.h +11 -2
  18. data/ext/makiri/core/mkr_hash.h +1 -1
  19. data/ext/makiri/core/mkr_span.h +186 -0
  20. data/ext/makiri/core/mkr_text.h +8 -8
  21. data/ext/makiri/core/mkr_utf8.c +101 -0
  22. data/ext/makiri/core/mkr_utf8.h +88 -0
  23. data/ext/makiri/extconf.rb +123 -10
  24. data/ext/makiri/fuzz/Makefile +95 -0
  25. data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
  26. data/ext/makiri/fuzz/xml_fuzz.c +24 -0
  27. data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
  28. data/ext/makiri/glue/glue.h +55 -11
  29. data/ext/makiri/glue/ruby_doc.c +129 -59
  30. data/ext/makiri/glue/ruby_html_css.c +292 -0
  31. data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
  32. data/ext/makiri/glue/ruby_html_node.c +859 -0
  33. data/ext/makiri/glue/ruby_html_serialize.c +154 -0
  34. data/ext/makiri/glue/ruby_node.c +74 -729
  35. data/ext/makiri/glue/ruby_node_set.c +167 -32
  36. data/ext/makiri/glue/ruby_xml.c +602 -0
  37. data/ext/makiri/glue/ruby_xml_node.c +1373 -0
  38. data/ext/makiri/glue/ruby_xpath.c +63 -30
  39. data/ext/makiri/glue/ruby_xpath.h +19 -0
  40. data/ext/makiri/lexbor_compat/compat.h +42 -9
  41. data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
  42. data/ext/makiri/lexbor_compat/dom_index.c +2 -2
  43. data/ext/makiri/lexbor_compat/post_parse.c +100 -10
  44. data/ext/makiri/lexbor_compat/source_loc.c +15 -13
  45. data/ext/makiri/lexbor_compat/text_index.c +14 -8
  46. data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
  47. data/ext/makiri/makiri.c +184 -6
  48. data/ext/makiri/makiri.h +43 -2
  49. data/ext/makiri/xml/mkr_xml.h +125 -0
  50. data/ext/makiri/xml/mkr_xml_chars.c +195 -0
  51. data/ext/makiri/xml/mkr_xml_index.c +169 -0
  52. data/ext/makiri/xml/mkr_xml_index.h +48 -0
  53. data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
  54. data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
  55. data/ext/makiri/xml/mkr_xml_node.c +399 -0
  56. data/ext/makiri/xml/mkr_xml_node.h +184 -0
  57. data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
  58. data/ext/makiri/xpath/mkr_css.c +1023 -0
  59. data/ext/makiri/xpath/mkr_css.h +65 -0
  60. data/ext/makiri/xpath/mkr_xpath.c +96 -32
  61. data/ext/makiri/xpath/mkr_xpath.h +109 -4
  62. data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
  63. data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
  64. data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
  65. data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
  66. data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
  67. data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
  68. data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
  69. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
  70. data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
  71. data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
  72. data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
  73. data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
  74. data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
  75. data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
  76. data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
  77. data/lib/makiri/{attribute.rb → attr.rb} +7 -3
  78. data/lib/makiri/cdata_section.rb +19 -0
  79. data/lib/makiri/comment.rb +10 -0
  80. data/lib/makiri/compat_aliases.rb +30 -0
  81. data/lib/makiri/document.rb +9 -73
  82. data/lib/makiri/document_fragment.rb +14 -9
  83. data/lib/makiri/element.rb +4 -4
  84. data/lib/makiri/html/document.rb +106 -0
  85. data/lib/makiri/html/node_methods.rb +19 -0
  86. data/lib/makiri/html.rb +12 -0
  87. data/lib/makiri/node.rb +58 -15
  88. data/lib/makiri/node_set.rb +8 -0
  89. data/lib/makiri/processing_instruction.rb +10 -0
  90. data/lib/makiri/text.rb +1 -1
  91. data/lib/makiri/version.rb +1 -1
  92. data/lib/makiri/xml/builder.rb +263 -0
  93. data/lib/makiri/xml/document.rb +24 -0
  94. data/lib/makiri/xml/node_methods.rb +84 -0
  95. data/lib/makiri/xml.rb +10 -0
  96. data/lib/makiri/xpath_context.rb +1 -1
  97. data/lib/makiri.rb +24 -5
  98. data/script/build_native_gem.rb +2 -2
  99. data/script/check_alloc_failures.rb +266 -0
  100. data/script/check_c_safety.rb +77 -2
  101. data/script/check_c_safety_allowlist.yml +102 -0
  102. data/script/check_leaks.rb +64 -0
  103. data/script/leaks_harness.rb +64 -0
  104. data/vendor/lexbor/CMakeLists.txt +6 -0
  105. data/vendor/lexbor/README.md +12 -0
  106. data/vendor/lexbor/config.cmake +1 -1
  107. data/vendor/lexbor/source/lexbor/core/base.h +1 -1
  108. data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
  109. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
  110. data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
  111. data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
  112. data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
  113. data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
  114. data/vendor/lexbor/source/lexbor/html/base.h +1 -1
  115. data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
  116. data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
  117. data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
  118. data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
  119. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
  120. data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
  121. data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
  122. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  123. data/vendor/lexbor/source/lexbor/url/url.c +5 -2
  124. data/vendor/lexbor/source/lexbor/url/url.h +9 -0
  125. data/vendor/lexbor/version +1 -1
  126. metadata +53 -9
  127. data/ext/makiri/glue/ruby_css.c +0 -185
  128. data/ext/makiri/glue/ruby_serialize.c +0 -92
  129. data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
  130. data/lib/makiri/cdata.rb +0 -6
@@ -14,48 +14,10 @@
14
14
  * functions are added in Phase 2.
15
15
  */
16
16
 
17
- /* Count Unicode code points in a UTF-8 string. XPath 1.0 string-length
18
- * and substring offsets are measured in characters, not bytes — Lexbor
19
- * exposes content as UTF-8 byte strings, so we walk and count leading
20
- * bytes (any byte that isn't a 10xxxxxx continuation). */
21
- static size_t
22
- utf8_char_count(const char *s)
23
- {
24
- if (s == NULL) return 0;
25
- size_t n = 0;
26
- for (const unsigned char *p = (const unsigned char *)s; *p; ++p) {
27
- if ((*p & 0xC0) != 0x80) ++n;
28
- }
29
- return n;
30
- }
31
-
32
- /* Step forward by n UTF-8 characters from s; returns byte pointer.
33
- * Returns the trailing NUL if n exceeds the string length. */
34
- static const char *
35
- utf8_advance(const char *s, size_t n)
36
- {
37
- const unsigned char *p = (const unsigned char *)s;
38
- while (n > 0 && *p) {
39
- do { ++p; } while ((*p & 0xC0) == 0x80);
40
- --n;
41
- }
42
- return (const char *)p;
43
- }
44
-
45
- static const char *
46
- mkr_text_find(const char *hay, size_t hay_len, const char *needle, size_t needle_len)
47
- {
48
- if (hay == NULL || needle == NULL) return NULL;
49
- if (needle_len == 0) return hay;
50
- if (needle_len > hay_len) return NULL;
51
- size_t last = hay_len - needle_len;
52
- for (size_t i = 0; i <= last; ++i) {
53
- if (hay[i] == needle[0] && memcmp(hay + i, needle, needle_len) == 0) {
54
- return hay + i;
55
- }
56
- }
57
- return NULL;
58
- }
17
+ /* Character-counting (mkr_utf8_count_chars), character-advance
18
+ * (mkr_utf8_advance_chars) and substring search (mkr_bytes_find) are
19
+ * length-bounded core primitives - see core/mkr_utf8.h and core/mkr_span.h.
20
+ * The byte-equality compares below go through mkr_bytes_eq (core/mkr_span.h). */
59
21
 
60
22
  static int
61
23
  arity_check(size_t got, size_t want_min, size_t want_max, mkr_xpath_error_t *err, const char *name)
@@ -71,52 +33,71 @@ arity_check(size_t got, size_t want_min, size_t want_max, mkr_xpath_error_t *err
71
33
  return 0;
72
34
  }
73
35
 
36
+ /* Require +arg+ to be a node-set; returns it, or NULL with a TYPE error naming
37
+ * +fname+. The single front for the "argument must be a node-set" check that
38
+ * count()/sum()/the name functions all share. */
39
+ static const mkr_nodeset_t *
40
+ require_nodeset(const mkr_val_t *arg, const char *fname, mkr_xpath_error_t *err)
41
+ {
42
+ if (arg->type != MKR_XPATH_TYPE_NODESET) {
43
+ mkr_err_setf(err, MKR_XPATH_ERR_TYPE, "%s(): argument must be a node-set", fname);
44
+ return NULL;
45
+ }
46
+ return &arg->u.nodeset;
47
+ }
48
+
49
+ /* string-value of args[0], or of the context node when there is no argument -
50
+ * the "optional node-set arg defaults to self, as a string" idiom shared by
51
+ * string() / string-length() / normalize-space(). */
52
+ static int
53
+ fn_string_arg_or_self(MKR_DOM_NODE *self_node, const mkr_val_t *args, size_t nargs,
54
+ mkr_xpath_limits_t *L, mkr_xpath_error_t *err, mkr_owned_text_t *out)
55
+ {
56
+ return (nargs == 0)
57
+ ? mkr_node_to_owned_text_or_fail(self_node, L, err, out)
58
+ : mkr_val_to_owned_text_or_fail(&args[0], L, err, out);
59
+ }
60
+
74
61
  static int
75
- fn_last(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
76
- size_t self_pos, size_t self_size,
62
+ fn_last(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
77
63
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
78
64
  {
79
- (void)ctx; (void)self_node; (void)self_pos; (void)args;
65
+ (void)ctx; (void)args;
80
66
  if (arity_check(nargs, 0, 0, err, "last") != 0) return -1;
81
67
  out->type = MKR_XPATH_TYPE_NUMBER;
82
- out->u.number = (double)self_size;
68
+ out->u.number = (double)focus->size;
83
69
  return 0;
84
70
  }
85
71
 
86
72
  static int
87
- fn_position(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
88
- size_t self_pos, size_t self_size,
73
+ fn_position(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
89
74
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
90
75
  {
91
- (void)ctx; (void)self_node; (void)self_size; (void)args;
76
+ (void)ctx; (void)args;
92
77
  if (arity_check(nargs, 0, 0, err, "position") != 0) return -1;
93
78
  out->type = MKR_XPATH_TYPE_NUMBER;
94
- out->u.number = (double)self_pos;
79
+ out->u.number = (double)focus->pos;
95
80
  return 0;
96
81
  }
97
82
 
98
83
  static int
99
- fn_count(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
100
- size_t self_pos, size_t self_size,
84
+ fn_count(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
101
85
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
102
86
  {
103
- (void)ctx; (void)self_node; (void)self_pos; (void)self_size;
87
+ (void)ctx; (void)focus;
104
88
  if (arity_check(nargs, 1, 1, err, "count") != 0) return -1;
105
- if (args[0].type != MKR_XPATH_TYPE_NODESET) {
106
- mkr_err_set(err, MKR_XPATH_ERR_TYPE, "count(): argument must be a node-set");
107
- return -1;
108
- }
89
+ const mkr_nodeset_t *ns = require_nodeset(&args[0], "count", err);
90
+ if (ns == NULL) return -1;
109
91
  out->type = MKR_XPATH_TYPE_NUMBER;
110
- out->u.number = (double)args[0].u.nodeset.count;
92
+ out->u.number = (double)ns->count;
111
93
  return 0;
112
94
  }
113
95
 
114
96
  static int
115
- fn_not(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
116
- size_t self_pos, size_t self_size,
97
+ fn_not(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
117
98
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
118
99
  {
119
- (void)ctx; (void)self_node; (void)self_pos; (void)self_size;
100
+ (void)ctx; (void)focus;
120
101
  if (arity_check(nargs, 1, 1, err, "not") != 0) return -1;
121
102
  out->type = MKR_XPATH_TYPE_BOOLEAN;
122
103
  out->u.boolean = !mkr_val_to_boolean(&args[0]);
@@ -124,11 +105,10 @@ fn_not(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
124
105
  }
125
106
 
126
107
  static int
127
- fn_true(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
128
- size_t self_pos, size_t self_size,
108
+ fn_true(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
129
109
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
130
110
  {
131
- (void)ctx; (void)self_node; (void)self_pos; (void)self_size; (void)args;
111
+ (void)ctx; (void)focus; (void)args;
132
112
  if (arity_check(nargs, 0, 0, err, "true") != 0) return -1;
133
113
  out->type = MKR_XPATH_TYPE_BOOLEAN;
134
114
  out->u.boolean = 1;
@@ -136,91 +116,113 @@ fn_true(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
136
116
  }
137
117
 
138
118
  static int
139
- fn_false(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
140
- size_t self_pos, size_t self_size,
119
+ fn_false(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
141
120
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
142
121
  {
143
- (void)ctx; (void)self_node; (void)self_pos; (void)self_size; (void)args;
122
+ (void)ctx; (void)focus; (void)args;
144
123
  if (arity_check(nargs, 0, 0, err, "false") != 0) return -1;
145
124
  out->type = MKR_XPATH_TYPE_BOOLEAN;
146
125
  out->u.boolean = 0;
147
126
  return 0;
148
127
  }
149
128
 
150
- /* id(string|nodeset) — looks up by HTML id attribute. */
151
- static lxb_dom_node_t *
152
- find_by_id(lxb_dom_node_t *root, const char *id, size_t id_len)
129
+ #ifndef MKR_HOST_XML
130
+ /* id() helpers are HTML-only: the XML instance's id() is the empty node-set
131
+ * (no DTD-declared IDs, §8.6), so it never walks the tree by id attribute.
132
+ * Guarded so the XML engine TU does not carry (and warn about) dead code. */
133
+
134
+ /* id(string|nodeset) - looks up by HTML id attribute. Walks the whole tree per
135
+ * token, so it charges each visited node to the op budget and fails closed on
136
+ * overrun (returns -1, *out untouched-meaningful); a hit sets *out, a miss
137
+ * leaves *out NULL. Without this, id() over a large node-set (a token per node,
138
+ * a tree walk per token) drives O(nodes^2) work as ~0 ops. */
139
+ static int
140
+ find_by_id(MKR_DOM_NODE *root, const char *id, size_t id_len,
141
+ mkr_xpath_limits_t *L, mkr_xpath_error_t *err, MKR_DOM_NODE **out)
153
142
  {
154
- if (root == NULL || id == NULL || id_len == 0) return NULL;
155
- lxb_dom_node_t *n = root;
143
+ *out = NULL;
144
+ if (root == NULL || id == NULL || id_len == 0) return 0;
145
+ MKR_DOM_NODE *n = root;
156
146
  while (n) {
157
- if (n->type == LXB_DOM_NODE_TYPE_ELEMENT) {
158
- lxb_dom_element_t *el = (lxb_dom_element_t *)n;
147
+ if (mkr_limit_eval_op(L, err) != 0) return -1;
148
+ if (MKR_NODE_TYPE(n) == MKR_NTYPE_ELEMENT) {
149
+ MKR_DOM_ELEMENT *el = MKR_NODE_AS_ELEMENT(n);
159
150
  size_t vlen = 0;
160
- const lxb_char_t *v = lxb_dom_element_get_attribute(el, (const lxb_char_t *)"id", 2, &vlen);
161
- if (v && vlen == id_len && memcmp(v, id, id_len) == 0) {
162
- return n;
151
+ const lxb_char_t *v = MKR_ELEM_GET_ATTRIBUTE(el, "id", 2, &vlen);
152
+ if (v && mkr_bytes_eq(v, vlen, id, id_len)) {
153
+ *out = n;
154
+ return 0;
163
155
  }
164
156
  }
165
- if (n->first_child) {
166
- n = n->first_child;
157
+ if (MKR_NODE_FIRST_CHILD(n)) {
158
+ n = MKR_NODE_FIRST_CHILD(n);
167
159
  } else {
168
- while (n && n != root && n->next == NULL) n = n->parent;
160
+ while (n && n != root && MKR_NODE_NEXT(n) == NULL) n = MKR_NODE_PARENT(n);
169
161
  if (n == root || n == NULL) break;
170
- n = n->next;
162
+ n = MKR_NODE_NEXT(n);
171
163
  }
172
164
  }
173
- return NULL;
165
+ return 0;
174
166
  }
175
167
 
176
168
  /*
177
- * Look up each whitespace-separated token in 's' (mutated) and push
178
- * every hit into 'out'. Duplicates are pushed unconditionally the
179
- * caller (fn_id) deduplicates the entire result via sort + adjacent
180
- * pass after all tokens have been processed, which is O(n log n)
181
- * versus O(n^2) per-insert contains() checks.
169
+ * Look up each whitespace-separated token in the borrowed text 's' and push
170
+ * every hit into 'out'. Duplicates are pushed unconditionally - the caller (fn_id) deduplicates
171
+ * the entire result via sort + adjacent pass after all tokens have been
172
+ * processed, which is O(n log n) versus O(n^2) per-insert contains() checks.
173
+ *
174
+ * Length-bounded over an mkr_span_t: each token is captured as a borrowed slice
175
+ * (mark/since) and handed to find_by_id by ptr+len, so the buffer is never
176
+ * mutated (the earlier temp-NUL splice was vestigial - find_by_id already
177
+ * compares by explicit length, never as a C string).
182
178
  */
183
179
  static int
184
- id_collect_from_string(char *s, lxb_dom_node_t *root, mkr_nodeset_t *out,
185
- mkr_xpath_context_t *ctx, mkr_xpath_error_t *err)
180
+ id_collect_from_string(mkr_borrowed_text_t s, MKR_DOM_NODE *root,
181
+ mkr_nodeset_t *out, mkr_xpath_context_t *ctx,
182
+ mkr_xpath_error_t *err)
186
183
  {
187
- if (s == NULL) return 0;
188
- char *p = s;
189
- while (*p) {
190
- while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
191
- if (*p == '\0') break;
192
- char *tok = p;
193
- while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
194
- size_t tok_len = (size_t)(p - tok);
195
- char saved = *p;
196
- *p = '\0';
197
- lxb_dom_node_t *hit = find_by_id(root, tok, tok_len);
198
- if (hit) {
199
- if (mkr_nodeset_push(out, hit, mkr_ctx_limits(ctx), err) != 0) {
200
- return -1;
201
- }
184
+ mkr_span_t sp = mkr_span(s.ptr, s.len);
185
+ for (;;) {
186
+ mkr_span_skip_xpath_ws(&sp);
187
+ if (mkr_span_peek(&sp) < 0) break;
188
+ const char *tok = mkr_span_mark(&sp);
189
+ while (mkr_span_peek(&sp) >= 0 && !mkr_xpath_is_ws(mkr_span_peek(&sp)))
190
+ mkr_span_skip(&sp, 1);
191
+ size_t tok_len = mkr_span_since(&sp, tok);
192
+ MKR_DOM_NODE *hit;
193
+ if (find_by_id(root, tok, tok_len, mkr_ctx_limits(ctx), err, &hit) != 0) {
194
+ return -1;
195
+ }
196
+ if (hit && mkr_nodeset_push(out, hit, mkr_ctx_limits(ctx), err) != 0) {
197
+ return -1;
202
198
  }
203
- if (saved == '\0') break;
204
- *p++ = saved;
205
199
  }
206
200
  return 0;
207
201
  }
202
+ #endif /* !MKR_HOST_XML */
208
203
 
209
204
  static int
210
- fn_id(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
211
- size_t self_pos, size_t self_size,
205
+ fn_id(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
212
206
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
213
207
  {
214
- (void)self_node; (void)self_pos; (void)self_size;
208
+ (void)focus;
215
209
  if (arity_check(nargs, 1, 1, err, "id") != 0) return -1;
216
210
  out->type = MKR_XPATH_TYPE_NODESET;
217
211
  mkr_nodeset_init(&out->u.nodeset);
218
212
 
219
- lxb_dom_node_t *root = (lxb_dom_node_t *)mkr_ctx_document(ctx);
213
+ #ifdef MKR_HOST_XML
214
+ /* Host policy (§8.6): in XML an ID is an attribute DECLARED ID-typed by the
215
+ * DTD/schema - not any attribute named "id". DTDs are rejected at parse
216
+ * (§9.4), so a document read here carries no ID-typed attributes and id() is
217
+ * the empty node-set. (xml:id is a separate, optional spec, not implemented.) */
218
+ (void)ctx; (void)args;
219
+ return 0;
220
+ #else
221
+ MKR_DOM_NODE *root = (MKR_DOM_NODE *)mkr_ctx_document(ctx);
220
222
  if (root == NULL) return 0;
221
223
 
222
224
  /* XPath 1.0 §4.1: id() argument may be a node-set; in that case each
223
- * node's string-value is treated as IDREFS whitespace-separated tokens.
225
+ * node's string-value is treated as IDREFS - whitespace-separated tokens.
224
226
  * For non-node-set, the value is converted to string and split likewise. */
225
227
  if (args[0].type == MKR_XPATH_TYPE_NODESET) {
226
228
  for (size_t i = 0; i < args[0].u.nodeset.count; ++i) {
@@ -229,7 +231,7 @@ fn_id(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
229
231
  mkr_nodeset_clear(&out->u.nodeset);
230
232
  return -1;
231
233
  }
232
- int rc = id_collect_from_string(text.ptr, root, &out->u.nodeset, ctx, err);
234
+ int rc = id_collect_from_string(mkr_borrowed_text_from_owned(text), root, &out->u.nodeset, ctx, err);
233
235
  mkr_owned_text_clear(&text);
234
236
  if (rc != 0) { mkr_nodeset_clear(&out->u.nodeset); return -1; }
235
237
  }
@@ -239,13 +241,14 @@ fn_id(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
239
241
  mkr_nodeset_clear(&out->u.nodeset);
240
242
  return -1;
241
243
  }
242
- int rc = id_collect_from_string(text.ptr, root, &out->u.nodeset, ctx, err);
244
+ int rc = id_collect_from_string(mkr_borrowed_text_from_owned(text), root, &out->u.nodeset, ctx, err);
243
245
  mkr_owned_text_clear(&text);
244
246
  if (rc != 0) { mkr_nodeset_clear(&out->u.nodeset); return -1; }
245
247
  }
246
248
  /* Result is in document order with duplicates removed (XPath 1.0 §4.1). */
247
249
  mkr_nodeset_unique_sorted(ctx, &out->u.nodeset);
248
250
  return 0;
251
+ #endif /* MKR_HOST_XML */
249
252
  }
250
253
 
251
254
  /* Forward decl for two_owned_texts() defined later. */
@@ -256,69 +259,68 @@ static int two_owned_texts(mkr_xpath_context_t *ctx, mkr_val_t *args,
256
259
  /* ---------- nokogiri-builtin namespace ---------- */
257
260
 
258
261
  /*
259
- * css-class(haystack, needle) true iff 'needle' is a whitespace-
262
+ * css-class(haystack, needle) - true iff 'needle' is a whitespace-
260
263
  * separated token of 'haystack'. The libxml2 version of this function
261
264
  * ships in nl_xpath_context.c (builtin_css_class); we keep behavior
262
265
  * identical.
263
266
  */
264
267
  static int
265
- ws_token_match(const char *str, const char *val, size_t val_len)
268
+ ws_token_match(mkr_borrowed_text_t hay, mkr_borrowed_text_t val)
266
269
  {
267
- if (str == NULL || val == NULL) return 0;
268
- if (val_len == 0) return 1; /* libxml2 returns non-NULL for empty val */
269
-
270
- const char *p = str;
271
- while (*p) {
272
- const char *tok = p;
273
- while (*p && !(*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
274
- size_t tok_len = (size_t)(p - tok);
275
- if (tok_len == val_len && tok_len > 0 && tok[0] == val[0]
276
- && memcmp(tok, val, val_len) == 0) {
270
+ /* NULL ordering preserved from the original: a NULL haystack/needle is a
271
+ * non-match BEFORE the empty-needle short-circuit (so empty val + NULL hay
272
+ * stays 0, not 1). */
273
+ if (hay.ptr == NULL || val.ptr == NULL) return 0;
274
+ if (val.len == 0) return 1; /* libxml2 returns non-NULL for empty val */
275
+
276
+ mkr_span_t sp = mkr_span(hay.ptr, hay.len);
277
+ while (mkr_span_peek(&sp) >= 0) {
278
+ const char *tok = mkr_span_mark(&sp);
279
+ while (mkr_span_peek(&sp) >= 0 && !mkr_xpath_is_ws(mkr_span_peek(&sp)))
280
+ mkr_span_skip(&sp, 1);
281
+ if (mkr_bytes_eq(tok, mkr_span_since(&sp, tok), val.ptr, val.len)) {
277
282
  return 1;
278
283
  }
279
- /* skip whitespace */
280
- while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
284
+ mkr_span_skip_xpath_ws(&sp);
281
285
  }
282
286
  return 0;
283
287
  }
284
288
 
285
289
  static int
286
- fn_nokogiri_css_class(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
287
- size_t self_pos, size_t self_size,
290
+ fn_nokogiri_css_class(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
288
291
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
289
292
  {
290
- (void)self_node; (void)self_pos; (void)self_size;
293
+ (void)focus;
291
294
  if (arity_check(nargs, 2, 2, err, "nokogiri-builtin:css-class") != 0) return -1;
292
295
  mkr_owned_text_t hay, needle;
293
296
  if (two_owned_texts(ctx, args, &hay, &needle, err) != 0) return -1;
294
297
  out->type = MKR_XPATH_TYPE_BOOLEAN;
295
- out->u.boolean = ws_token_match(hay.ptr, needle.ptr, needle.len);
298
+ out->u.boolean = ws_token_match(mkr_borrowed_text_from_owned(hay),
299
+ mkr_borrowed_text_from_owned(needle));
296
300
  mkr_owned_text_clear(&hay);
297
301
  mkr_owned_text_clear(&needle);
298
302
  return 0;
299
303
  }
300
304
 
301
305
  /*
302
- * local-name-is(elementName) true iff the context node's qualified
306
+ * local-name-is(elementName) - true iff the context node's qualified
303
307
  * name (for HTML this is the lowercase local name) equals the argument.
304
308
  */
305
309
  static int
306
- fn_nokogiri_local_name_is(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
307
- size_t self_pos, size_t self_size,
310
+ fn_nokogiri_local_name_is(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
308
311
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
309
312
  {
310
- (void)self_pos; (void)self_size;
311
313
  if (arity_check(nargs, 1, 1, err, "nokogiri-builtin:local-name-is") != 0) return -1;
312
314
  mkr_owned_text_t want;
313
315
  if (mkr_val_to_owned_text_or_fail(&args[0], mkr_ctx_limits(ctx), err, &want) != 0) return -1;
314
316
 
315
317
  out->type = MKR_XPATH_TYPE_BOOLEAN;
316
318
  out->u.boolean = 0;
317
- if (self_node != NULL) {
319
+ if (focus->node != NULL) {
318
320
  size_t got_len = 0;
319
- const lxb_char_t *got = mkr_dom_node_name_qualified(self_node, &got_len);
321
+ const lxb_char_t *got = MKR_ELEM_QUALIFIED_NAME(focus->node, &got_len);
320
322
  if (got != NULL) {
321
- out->u.boolean = (got_len == want.len && memcmp(got, want.ptr, want.len) == 0);
323
+ out->u.boolean = mkr_bytes_eq(got, got_len, want.ptr, want.len);
322
324
  }
323
325
  }
324
326
  mkr_owned_text_clear(&want);
@@ -336,12 +338,65 @@ static const fn_entry_t fn_table_nokogiri_builtin[] = {
336
338
  { NULL, NULL },
337
339
  };
338
340
 
341
+ #ifdef MKR_HOST_XML
342
+ /* ---------- internal: untyped :*-of-type position (XML, CSS-only) ---------- *
343
+ * Two elements are the same "type" iff they share an expanded name (local name
344
+ * + namespace URI). These back the CSS-lowered untyped of-type pseudo-classes;
345
+ * see MKR_FN_OF_TYPE_POS in mkr_xpath_internal.h for why they cannot be a pure
346
+ * XPath expression. focus->node is the context element being filtered. */
339
347
  static int
340
- fn_boolean(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
341
- size_t self_pos, size_t self_size,
348
+ mkr_xml_same_type(MKR_DOM_NODE *a, MKR_DOM_NODE *b)
349
+ {
350
+ size_t al = 0, bl = 0;
351
+ const lxb_char_t *an = MKR_ELEM_LOCAL_NAME(a, &al);
352
+ const lxb_char_t *bn = MKR_ELEM_LOCAL_NAME(b, &bl);
353
+ if (!mkr_bytes_eq(an, al, bn, bl)) return 0;
354
+ size_t au = 0, bu = 0;
355
+ const char *ap = MKR_NODE_NS_URI(a, NULL, &au);
356
+ const char *bp = MKR_NODE_NS_URI(b, NULL, &bu);
357
+ return mkr_bytes_eq(ap, au, bp, bu);
358
+ }
359
+
360
+ /* 1-based position of +self+ among its same-type element siblings; forward walks
361
+ * preceding siblings (position from the start), else following (from the end). */
362
+ static double
363
+ mkr_xml_of_type_pos(MKR_DOM_NODE *self, int forward)
364
+ {
365
+ if (self == NULL || MKR_NODE_TYPE(self) != MKR_NTYPE_ELEMENT) return 0.0;
366
+ long pos = 1;
367
+ for (MKR_DOM_NODE *s = forward ? MKR_NODE_PREV(self) : MKR_NODE_NEXT(self);
368
+ s != NULL; s = forward ? MKR_NODE_PREV(s) : MKR_NODE_NEXT(s)) {
369
+ if (MKR_NODE_TYPE(s) == MKR_NTYPE_ELEMENT && mkr_xml_same_type(self, s)) pos++;
370
+ }
371
+ return (double)pos;
372
+ }
373
+
374
+ static int
375
+ fn_xml_of_type_pos(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
376
+ mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
377
+ {
378
+ (void)ctx; (void)args; (void)nargs; (void)err;
379
+ out->type = MKR_XPATH_TYPE_NUMBER;
380
+ out->u.number = mkr_xml_of_type_pos(focus->node, 1);
381
+ return 0;
382
+ }
383
+
384
+ static int
385
+ fn_xml_of_type_pos_last(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
386
+ mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
387
+ {
388
+ (void)ctx; (void)args; (void)nargs; (void)err;
389
+ out->type = MKR_XPATH_TYPE_NUMBER;
390
+ out->u.number = mkr_xml_of_type_pos(focus->node, 0);
391
+ return 0;
392
+ }
393
+ #endif /* MKR_HOST_XML */
394
+
395
+ static int
396
+ fn_boolean(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
342
397
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
343
398
  {
344
- (void)ctx; (void)self_node; (void)self_pos; (void)self_size;
399
+ (void)ctx; (void)focus;
345
400
  if (arity_check(nargs, 1, 1, err, "boolean") != 0) return -1;
346
401
  out->type = MKR_XPATH_TYPE_BOOLEAN;
347
402
  out->u.boolean = mkr_val_to_boolean(&args[0]);
@@ -351,29 +406,22 @@ fn_boolean(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
351
406
  /* ---------- string functions ---------- */
352
407
 
353
408
  static int
354
- fn_string(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
355
- size_t self_pos, size_t self_size,
409
+ fn_string(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
356
410
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
357
411
  {
358
- (void)self_pos; (void)self_size;
359
412
  if (arity_check(nargs, 0, 1, err, "string") != 0) return -1;
360
413
  out->type = MKR_XPATH_TYPE_STRING;
361
414
  mkr_owned_text_t text;
362
- if (nargs == 0) {
363
- if (mkr_node_to_owned_text_or_fail(self_node, mkr_ctx_limits(ctx), err, &text) != 0) return -1;
364
- } else {
365
- if (mkr_val_to_owned_text_or_fail(&args[0], mkr_ctx_limits(ctx), err, &text) != 0) return -1;
366
- }
415
+ if (fn_string_arg_or_self(focus->node, args, nargs, mkr_ctx_limits(ctx), err, &text) != 0) return -1;
367
416
  mkr_val_set_owned_text(out, text);
368
417
  return 0;
369
418
  }
370
419
 
371
420
  static int
372
- fn_concat(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
373
- size_t self_pos, size_t self_size,
421
+ fn_concat(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
374
422
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
375
423
  {
376
- (void)self_node; (void)self_pos; (void)self_size;
424
+ (void)focus;
377
425
  if (nargs < 2) {
378
426
  mkr_err_set(err, MKR_XPATH_ERR_RUNTIME, "concat(): expected at least 2 arguments");
379
427
  return -1;
@@ -439,48 +487,49 @@ two_owned_texts(mkr_xpath_context_t *ctx, mkr_val_t *args,
439
487
  }
440
488
 
441
489
  static int
442
- fn_starts_with(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
443
- size_t self_pos, size_t self_size,
490
+ fn_starts_with(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
444
491
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
445
492
  {
446
- (void)self_node; (void)self_pos; (void)self_size;
493
+ (void)focus;
447
494
  if (arity_check(nargs, 2, 2, err, "starts-with") != 0) return -1;
448
495
  mkr_owned_text_t s, t;
449
496
  if (two_owned_texts(ctx, args, &s, &t, err) != 0) return -1;
450
497
  out->type = MKR_XPATH_TYPE_BOOLEAN;
451
- out->u.boolean = (s.len >= t.len && memcmp(s.ptr, t.ptr, t.len) == 0);
498
+ mkr_span_t ss = mkr_span(s.ptr, s.len);
499
+ out->u.boolean = mkr_span_starts(&ss, t.ptr, t.len);
452
500
  mkr_owned_text_clear(&s);
453
501
  mkr_owned_text_clear(&t);
454
502
  return 0;
455
503
  }
456
504
 
457
505
  static int
458
- fn_contains(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
459
- size_t self_pos, size_t self_size,
506
+ fn_contains(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
460
507
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
461
508
  {
462
- (void)self_node; (void)self_pos; (void)self_size;
509
+ (void)focus;
463
510
  if (arity_check(nargs, 2, 2, err, "contains") != 0) return -1;
464
511
  mkr_owned_text_t s, t;
465
512
  if (two_owned_texts(ctx, args, &s, &t, err) != 0) return -1;
466
513
  out->type = MKR_XPATH_TYPE_BOOLEAN;
467
- out->u.boolean = (mkr_text_find(s.ptr, s.len, t.ptr, t.len) != NULL);
514
+ size_t idx;
515
+ out->u.boolean = mkr_bytes_find(s.ptr, s.len, t.ptr, t.len, &idx);
468
516
  mkr_owned_text_clear(&s);
469
517
  mkr_owned_text_clear(&t);
470
518
  return 0;
471
519
  }
472
520
 
473
521
  static int
474
- fn_substring_before(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
475
- size_t self_pos, size_t self_size,
522
+ fn_substring_before(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
476
523
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
477
524
  {
478
- (void)self_node; (void)self_pos; (void)self_size;
525
+ (void)focus;
479
526
  if (arity_check(nargs, 2, 2, err, "substring-before") != 0) return -1;
480
527
  mkr_owned_text_t s, t;
481
528
  if (two_owned_texts(ctx, args, &s, &t, err) != 0) return -1;
482
- const char *found = (t.len == 0) ? NULL : mkr_text_find(s.ptr, s.len, t.ptr, t.len);
483
- size_t n = (found != NULL) ? (size_t)(found - s.ptr) : 0;
529
+ /* substring-before(s, t): bytes of s up to the first occurrence of t, or ""
530
+ * when t is empty or absent. */
531
+ size_t n = 0, idx;
532
+ if (t.len != 0 && mkr_bytes_find(s.ptr, s.len, t.ptr, t.len, &idx)) n = idx;
484
533
  char *p = mkr_strndup(s.ptr, n); /* n == 0 yields an owned "" */
485
534
  mkr_owned_text_clear(&s);
486
535
  mkr_owned_text_clear(&t);
@@ -493,11 +542,10 @@ fn_substring_before(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
493
542
  }
494
543
 
495
544
  static int
496
- fn_substring_after(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
497
- size_t self_pos, size_t self_size,
545
+ fn_substring_after(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
498
546
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
499
547
  {
500
- (void)self_node; (void)self_pos; (void)self_size;
548
+ (void)focus;
501
549
  if (arity_check(nargs, 2, 2, err, "substring-after") != 0) return -1;
502
550
  mkr_owned_text_t s, t;
503
551
  if (two_owned_texts(ctx, args, &s, &t, err) != 0) return -1;
@@ -507,9 +555,14 @@ fn_substring_after(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
507
555
  out_len = s.len;
508
556
  p = mkr_strndup(s.ptr, s.len);
509
557
  } else {
510
- const char *found = mkr_text_find(s.ptr, s.len, t.ptr, t.len);
511
- out_len = found ? s.len - (size_t)((found + t.len) - s.ptr) : 0;
512
- p = found ? mkr_strndup(found + t.len, out_len) : mkr_strdup("");
558
+ size_t idx;
559
+ if (mkr_bytes_find(s.ptr, s.len, t.ptr, t.len, &idx)) {
560
+ out_len = s.len - (idx + t.len);
561
+ p = mkr_strndup(s.ptr + idx + t.len, out_len);
562
+ } else {
563
+ out_len = 0;
564
+ p = mkr_strdup("");
565
+ }
513
566
  }
514
567
  mkr_owned_text_clear(&s);
515
568
  mkr_owned_text_clear(&t);
@@ -521,21 +574,20 @@ fn_substring_after(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
521
574
  return 0;
522
575
  }
523
576
 
524
- /* substring(s, start[, length]) XPath positions are 1-based and round
577
+ /* substring(s, start[, length]) - XPath positions are 1-based and round
525
578
  * to nearest; out-of-range positions clip to the string. */
526
579
  static int
527
- fn_substring(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
528
- size_t self_pos, size_t self_size,
580
+ fn_substring(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
529
581
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
530
582
  {
531
- (void)self_node; (void)self_pos; (void)self_size;
583
+ (void)focus;
532
584
  if (arity_check(nargs, 2, 3, err, "substring") != 0) return -1;
533
585
  mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
534
586
  mkr_owned_text_t s;
535
587
  if (mkr_val_to_owned_text_or_fail(&args[0], L, err, &s) != 0) return -1;
536
588
  double start_d, end_d;
537
589
  if (mkr_val_to_number_or_fail(&args[1], L, err, &start_d) != 0) { mkr_owned_text_clear(&s); return -1; }
538
- size_t s_chars = utf8_char_count(s.ptr);
590
+ size_t s_chars = mkr_utf8_count_chars(s.ptr, s.len);
539
591
  if (nargs == 3) {
540
592
  double len_d;
541
593
  if (mkr_val_to_number_or_fail(&args[2], L, err, &len_d) != 0) { mkr_owned_text_clear(&s); return -1; }
@@ -567,10 +619,12 @@ fn_substring(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
567
619
  if (end <= start) {
568
620
  p = mkr_strdup("");
569
621
  } else {
570
- const char *byte_start = utf8_advance(s.ptr, (size_t)(start - 1));
571
- const char *byte_end = utf8_advance(byte_start, (size_t)(end - start));
572
- out_len = (size_t)(byte_end - byte_start);
573
- p = mkr_strndup(byte_start, out_len);
622
+ size_t start_off = mkr_utf8_advance_chars(s.ptr, s.len, (size_t)(start - 1));
623
+ size_t end_off = start_off + mkr_utf8_advance_chars(s.ptr + start_off,
624
+ s.len - start_off,
625
+ (size_t)(end - start));
626
+ out_len = end_off - start_off;
627
+ p = mkr_strndup(s.ptr + start_off, out_len);
574
628
  }
575
629
  }
576
630
  mkr_owned_text_clear(&s);
@@ -583,38 +637,26 @@ fn_substring(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
583
637
  }
584
638
 
585
639
  static int
586
- fn_string_length(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
587
- size_t self_pos, size_t self_size,
640
+ fn_string_length(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
588
641
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
589
642
  {
590
- (void)self_pos; (void)self_size;
591
643
  if (arity_check(nargs, 0, 1, err, "string-length") != 0) return -1;
592
- mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
593
644
  mkr_owned_text_t s;
594
- int rc = (nargs == 0)
595
- ? mkr_node_to_owned_text_or_fail(self_node, L, err, &s)
596
- : mkr_val_to_owned_text_or_fail(&args[0], L, err, &s);
597
- if (rc != 0) return -1;
645
+ if (fn_string_arg_or_self(focus->node, args, nargs, mkr_ctx_limits(ctx), err, &s) != 0) return -1;
598
646
  out->type = MKR_XPATH_TYPE_NUMBER;
599
- out->u.number = (double)utf8_char_count(s.ptr);
647
+ out->u.number = (double)mkr_utf8_count_chars(s.ptr, s.len);
600
648
  mkr_owned_text_clear(&s);
601
649
  return 0;
602
650
  }
603
651
 
604
652
  /* normalize-space: collapse runs of whitespace, trim leading/trailing */
605
653
  static int
606
- fn_normalize_space(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
607
- size_t self_pos, size_t self_size,
654
+ fn_normalize_space(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
608
655
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
609
656
  {
610
- (void)self_pos; (void)self_size;
611
657
  if (arity_check(nargs, 0, 1, err, "normalize-space") != 0) return -1;
612
- mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
613
658
  mkr_owned_text_t s;
614
- int rc = (nargs == 0)
615
- ? mkr_node_to_owned_text_or_fail(self_node, L, err, &s)
616
- : mkr_val_to_owned_text_or_fail(&args[0], L, err, &s);
617
- if (rc != 0) return -1;
659
+ if (fn_string_arg_or_self(focus->node, args, nargs, mkr_ctx_limits(ctx), err, &s) != 0) return -1;
618
660
  char *buf = mkr_str_alloc(s.len);
619
661
  if (buf == NULL) { mkr_owned_text_clear(&s); mkr_err_set(err, MKR_XPATH_ERR_OOM, "out of memory in normalize-space"); return -1; }
620
662
  size_t out_i = 0;
@@ -638,11 +680,10 @@ fn_normalize_space(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
638
680
  }
639
681
 
640
682
  static int
641
- fn_translate(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
642
- size_t self_pos, size_t self_size,
683
+ fn_translate(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
643
684
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
644
685
  {
645
- (void)self_node; (void)self_pos; (void)self_size;
686
+ (void)focus;
646
687
  if (arity_check(nargs, 3, 3, err, "translate") != 0) return -1;
647
688
  mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
648
689
  mkr_owned_text_t s, from, to;
@@ -675,7 +716,7 @@ fn_translate(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
675
716
  }
676
717
 
677
718
  /* The string-literal lexer validates UTF-8 and DOM string-values are valid
678
- * UTF-8, so a decode error here should not happen but if it does, fail
719
+ * UTF-8, so a decode error here should not happen - but if it does, fail
679
720
  * closed (RUNTIME error) rather than silently truncating the result. */
680
721
  size_t from_n = 0;
681
722
  for (const lxb_char_t *p = (const lxb_char_t *)from.ptr, *e = p + from.len; p < e;) {
@@ -750,18 +791,16 @@ decode_fail:
750
791
  /* ---------- number functions ---------- */
751
792
 
752
793
  static int
753
- fn_number(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
754
- size_t self_pos, size_t self_size,
794
+ fn_number(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
755
795
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
756
796
  {
757
- (void)self_pos; (void)self_size;
758
797
  if (arity_check(nargs, 0, 1, err, "number") != 0) return -1;
759
798
  out->type = MKR_XPATH_TYPE_NUMBER;
760
799
  mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
761
800
  if (nargs == 0) {
762
801
  mkr_val_t self_v = { .type = MKR_XPATH_TYPE_NODESET };
763
802
  mkr_nodeset_init(&self_v.u.nodeset);
764
- if (self_node && mkr_nodeset_push(&self_v.u.nodeset, self_node, L, err) != 0) {
803
+ if (focus->node && mkr_nodeset_push(&self_v.u.nodeset, focus->node, L, err) != 0) {
765
804
  mkr_nodeset_clear(&self_v.u.nodeset);
766
805
  return -1;
767
806
  }
@@ -773,22 +812,20 @@ fn_number(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
773
812
  }
774
813
 
775
814
  static int
776
- fn_sum(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
777
- size_t self_pos, size_t self_size,
815
+ fn_sum(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
778
816
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
779
817
  {
780
- (void)self_node; (void)self_pos; (void)self_size;
818
+ (void)focus;
781
819
  if (arity_check(nargs, 1, 1, err, "sum") != 0) return -1;
782
- if (args[0].type != MKR_XPATH_TYPE_NODESET) {
783
- mkr_err_set(err, MKR_XPATH_ERR_TYPE, "sum(): argument must be a node-set");
784
- return -1;
785
- }
820
+ const mkr_nodeset_t *ns = require_nodeset(&args[0], "sum", err);
821
+ if (ns == NULL) return -1;
786
822
  /* mkr_get_cached_node_text consults ctx->limits internally for the
787
823
  * cache-miss path; no need to thread max_string_bytes here. */
788
824
  double total = 0.0;
789
- for (size_t i = 0; i < args[0].u.nodeset.count; ++i) {
825
+ for (size_t i = 0; i < ns->count; ++i) {
826
+ if (mkr_limit_eval_op(mkr_ctx_limits(ctx), err) != 0) return -1;
790
827
  mkr_borrowed_text_t s;
791
- if (mkr_get_cached_node_text(ctx, args[0].u.nodeset.items[i], &s, err) != 0) {
828
+ if (mkr_get_cached_node_text(ctx, ns->items[i], &s, err) != 0) {
792
829
  return -1;
793
830
  }
794
831
  total += mkr_borrowed_text_to_number(s);
@@ -799,11 +836,10 @@ fn_sum(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
799
836
  }
800
837
 
801
838
  static int
802
- fn_floor(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
803
- size_t self_pos, size_t self_size,
839
+ fn_floor(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
804
840
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
805
841
  {
806
- (void)self_node; (void)self_pos; (void)self_size;
842
+ (void)focus;
807
843
  if (arity_check(nargs, 1, 1, err, "floor") != 0) return -1;
808
844
  double d;
809
845
  if (mkr_val_to_number_or_fail(&args[0], mkr_ctx_limits(ctx), err, &d) != 0) return -1;
@@ -813,11 +849,10 @@ fn_floor(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
813
849
  }
814
850
 
815
851
  static int
816
- fn_ceiling(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
817
- size_t self_pos, size_t self_size,
852
+ fn_ceiling(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
818
853
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
819
854
  {
820
- (void)self_node; (void)self_pos; (void)self_size;
855
+ (void)focus;
821
856
  if (arity_check(nargs, 1, 1, err, "ceiling") != 0) return -1;
822
857
  double d;
823
858
  if (mkr_val_to_number_or_fail(&args[0], mkr_ctx_limits(ctx), err, &d) != 0) return -1;
@@ -828,11 +863,10 @@ fn_ceiling(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
828
863
 
829
864
  /* XPath 1.0 round(): round to nearest integer, .5 rounds toward +inf. */
830
865
  static int
831
- fn_round(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
832
- size_t self_pos, size_t self_size,
866
+ fn_round(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
833
867
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
834
868
  {
835
- (void)self_node; (void)self_pos; (void)self_size;
869
+ (void)focus;
836
870
  if (arity_check(nargs, 1, 1, err, "round") != 0) return -1;
837
871
  double d;
838
872
  if (mkr_val_to_number_or_fail(&args[0], mkr_ctx_limits(ctx), err, &d) != 0) return -1;
@@ -846,16 +880,14 @@ fn_round(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
846
880
 
847
881
  /* Helper: pull the first node of a nodeset arg, or the self node when
848
882
  * the arg list is empty. */
849
- static lxb_dom_node_t *
850
- name_func_target(mkr_val_t *args, size_t nargs, lxb_dom_node_t *self_node, mkr_xpath_error_t *err, const char *fname)
883
+ static MKR_DOM_NODE *
884
+ name_func_target(mkr_val_t *args, size_t nargs, MKR_DOM_NODE *self_node, mkr_xpath_error_t *err, const char *fname)
851
885
  {
852
886
  if (nargs == 0) return self_node;
853
- if (args[0].type != MKR_XPATH_TYPE_NODESET) {
854
- mkr_err_setf(err, MKR_XPATH_ERR_TYPE, "%s(): argument must be a node-set", fname);
855
- return NULL;
856
- }
857
- if (args[0].u.nodeset.count == 0) return NULL;
858
- return args[0].u.nodeset.items[0];
887
+ const mkr_nodeset_t *ns = require_nodeset(&args[0], fname, err);
888
+ if (ns == NULL) return NULL;
889
+ if (ns->count == 0) return NULL;
890
+ return ns->items[0];
859
891
  }
860
892
 
861
893
  /* Set out to an empty string and report OOM if strdup fails. */
@@ -889,82 +921,79 @@ set_bytes_string(mkr_val_t *out, const lxb_char_t *src, size_t len,
889
921
  }
890
922
 
891
923
  static int
892
- fn_local_name(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
893
- size_t self_pos, size_t self_size,
924
+ fn_local_name(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
894
925
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
895
926
  {
896
- (void)ctx; (void)self_pos; (void)self_size;
927
+ (void)ctx;
897
928
  if (arity_check(nargs, 0, 1, err, "local-name") != 0) return -1;
898
929
  /* Every return path below sets out via set_empty_string / set_bytes_string;
899
930
  * an error return leaves the caller's zero-initialised (clearable) out. */
900
- lxb_dom_node_t *n = name_func_target(args, nargs, self_node, err, "local-name");
931
+ MKR_DOM_NODE *n = name_func_target(args, nargs, focus->node, err, "local-name");
901
932
  if (err->status != MKR_XPATH_OK) return -1;
902
933
  if (n == NULL ||
903
- (n->type != LXB_DOM_NODE_TYPE_ELEMENT &&
904
- n->type != LXB_DOM_NODE_TYPE_ATTRIBUTE &&
905
- n->type != LXB_DOM_NODE_TYPE_PROCESSING_INSTRUCTION)) {
934
+ (MKR_NODE_TYPE(n) != MKR_NTYPE_ELEMENT &&
935
+ MKR_NODE_TYPE(n) != MKR_NTYPE_ATTRIBUTE &&
936
+ MKR_NODE_TYPE(n) != MKR_NTYPE_PI)) {
906
937
  return set_empty_string(out, err, "local-name");
907
938
  }
908
939
  size_t len = 0;
909
940
  const lxb_char_t *name;
910
- if (n->type == LXB_DOM_NODE_TYPE_ATTRIBUTE) {
911
- name = lxb_dom_attr_local_name((lxb_dom_attr_t *)n, &len);
912
- } else if (n->type == LXB_DOM_NODE_TYPE_ELEMENT) {
913
- name = lxb_dom_element_local_name((lxb_dom_element_t *)n, &len);
941
+ if (MKR_NODE_TYPE(n) == MKR_NTYPE_ATTRIBUTE) {
942
+ name = MKR_ATTR_LOCAL_NAME(n, &len);
943
+ } else if (MKR_NODE_TYPE(n) == MKR_NTYPE_ELEMENT) {
944
+ name = MKR_ELEM_LOCAL_NAME(n, &len);
914
945
  } else {
915
- name = lxb_dom_node_name(n, &len);
946
+ name = MKR_NODE_PI_NAME(n, &len);
916
947
  }
917
948
  return set_bytes_string(out, name, len, err, "local-name");
918
949
  }
919
950
 
920
951
  static int
921
- fn_namespace_uri(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
922
- size_t self_pos, size_t self_size,
952
+ fn_namespace_uri(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
923
953
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
924
954
  {
925
- (void)self_pos; (void)self_size;
926
955
  if (arity_check(nargs, 0, 1, err, "namespace-uri") != 0) return -1;
927
- lxb_dom_node_t *n = name_func_target(args, nargs, self_node, err, "namespace-uri");
956
+ MKR_DOM_NODE *n = name_func_target(args, nargs, focus->node, err, "namespace-uri");
928
957
  if (err->status != MKR_XPATH_OK) return -1;
929
958
  if (n == NULL ||
930
- (n->type != LXB_DOM_NODE_TYPE_ELEMENT && n->type != LXB_DOM_NODE_TYPE_ATTRIBUTE)) {
959
+ (MKR_NODE_TYPE(n) != MKR_NTYPE_ELEMENT && MKR_NODE_TYPE(n) != MKR_NTYPE_ATTRIBUTE)) {
931
960
  return set_empty_string(out, err, "namespace-uri");
932
961
  }
933
- if (n->ns == 0) {
962
+ if (MKR_NODE_NS_ID(n) == 0) {
934
963
  return set_empty_string(out, err, "namespace-uri");
935
964
  }
936
- lxb_dom_document_t *doc = mkr_ctx_document(ctx);
937
- if (doc == NULL || doc->ns == NULL) {
965
+ size_t len = 0;
966
+ const char *uri = MKR_NODE_NS_URI(n, mkr_ctx_document(ctx), &len);
967
+ if (uri == NULL) {
938
968
  return set_empty_string(out, err, "namespace-uri");
939
969
  }
940
- size_t len = 0;
941
- const lxb_char_t *uri = lxb_ns_by_id(doc->ns, n->ns, &len);
942
- return set_bytes_string(out, uri, len, err, "namespace-uri");
970
+ return set_bytes_string(out, (const lxb_char_t *)uri, len, err, "namespace-uri");
943
971
  }
944
972
 
945
973
  static int
946
- fn_name(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
947
- size_t self_pos, size_t self_size,
974
+ fn_name(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
948
975
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
949
976
  {
950
- (void)ctx; (void)self_pos; (void)self_size;
977
+ (void)ctx;
951
978
  if (arity_check(nargs, 0, 1, err, "name") != 0) return -1;
952
- lxb_dom_node_t *n = name_func_target(args, nargs, self_node, err, "name");
979
+ MKR_DOM_NODE *n = name_func_target(args, nargs, focus->node, err, "name");
953
980
  if (err->status != MKR_XPATH_OK) return -1;
954
981
  if (n == NULL ||
955
- (n->type != LXB_DOM_NODE_TYPE_ELEMENT &&
956
- n->type != LXB_DOM_NODE_TYPE_ATTRIBUTE &&
957
- n->type != LXB_DOM_NODE_TYPE_PROCESSING_INSTRUCTION)) {
982
+ (MKR_NODE_TYPE(n) != MKR_NTYPE_ELEMENT &&
983
+ MKR_NODE_TYPE(n) != MKR_NTYPE_ATTRIBUTE &&
984
+ MKR_NODE_TYPE(n) != MKR_NTYPE_PI)) {
958
985
  return set_empty_string(out, err, "name");
959
986
  }
960
987
  /* In HTML the qualified name matches the local name; this also avoids
961
988
  * surfacing the LXB_NS_HTML prefix that would otherwise confuse users. */
962
989
  size_t len = 0;
963
990
  const lxb_char_t *name;
964
- if (n->type == LXB_DOM_NODE_TYPE_ATTRIBUTE) {
965
- name = lxb_dom_attr_qualified_name((lxb_dom_attr_t *)n, &len);
991
+ if (MKR_NODE_TYPE(n) == MKR_NTYPE_ATTRIBUTE) {
992
+ name = MKR_ATTR_QUALIFIED_NAME(n, &len);
993
+ } else if (MKR_NODE_TYPE(n) == MKR_NTYPE_ELEMENT) {
994
+ name = MKR_ELEM_QUALIFIED_NAME(n, &len);
966
995
  } else {
967
- name = mkr_dom_node_name_qualified(n, &len);
996
+ name = MKR_NODE_PI_NAME(n, &len); /* a PI's expanded-name is (null, target) */
968
997
  }
969
998
  return set_bytes_string(out, name, len, err, "name");
970
999
  }
@@ -972,25 +1001,30 @@ fn_name(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
972
1001
  /* ---------- boolean / language ---------- */
973
1002
 
974
1003
  static int
975
- fn_lang(mkr_xpath_context_t *ctx, lxb_dom_node_t *self_node,
976
- size_t self_pos, size_t self_size,
1004
+ fn_lang(mkr_xpath_context_t *ctx, const mkr_focus_t *focus,
977
1005
  mkr_val_t *args, size_t nargs, mkr_val_t *out, mkr_xpath_error_t *err)
978
1006
  {
979
- (void)ctx; (void)self_pos; (void)self_size;
1007
+ (void)ctx;
980
1008
  if (arity_check(nargs, 1, 1, err, "lang") != 0) return -1;
981
1009
  mkr_owned_text_t want;
982
1010
  if (mkr_val_to_owned_text_or_fail(&args[0], mkr_ctx_limits(ctx), err, &want) != 0) return -1;
983
1011
  out->type = MKR_XPATH_TYPE_BOOLEAN;
984
1012
  out->u.boolean = 0;
985
- /* Walk up the ancestor chain looking for an @xml:lang or @lang. */
986
- for (lxb_dom_node_t *p = self_node; p != NULL; p = p->parent) {
987
- if (p->type != LXB_DOM_NODE_TYPE_ELEMENT) continue;
988
- lxb_dom_element_t *el = (lxb_dom_element_t *)p;
1013
+ /* Walk up the ancestor chain looking for the host's language attribute.
1014
+ Host policy (§8.6): XPath 1.0 lang() is xml:lang based; HTML uses the `lang`
1015
+ attribute (with xml:lang accepted as a fallback). */
1016
+ for (MKR_DOM_NODE *p = focus->node; p != NULL; p = MKR_NODE_PARENT(p)) {
1017
+ if (MKR_NODE_TYPE(p) != MKR_NTYPE_ELEMENT) continue;
1018
+ MKR_DOM_ELEMENT *el = MKR_NODE_AS_ELEMENT(p);
989
1019
  size_t vlen = 0;
990
- const lxb_char_t *v = lxb_dom_element_get_attribute(el, (const lxb_char_t *)"lang", 4, &vlen);
1020
+ #ifdef MKR_HOST_XML
1021
+ const lxb_char_t *v = MKR_ELEM_GET_ATTRIBUTE(el, "xml:lang", 8, &vlen);
1022
+ #else
1023
+ const lxb_char_t *v = MKR_ELEM_GET_ATTRIBUTE(el, "lang", 4, &vlen);
991
1024
  if (v == NULL) {
992
- v = lxb_dom_element_get_attribute(el, (const lxb_char_t *)"xml:lang", 8, &vlen);
1025
+ v = MKR_ELEM_GET_ATTRIBUTE(el, "xml:lang", 8, &vlen);
993
1026
  }
1027
+ #endif /* MKR_HOST_XML */
994
1028
  if (v == NULL) continue;
995
1029
  /* Compare prefix (case-insensitive) up to '-'. */
996
1030
  if (vlen >= want.len) {
@@ -1043,16 +1077,24 @@ static const fn_entry_t fn_table[] = {
1043
1077
  { "floor", fn_floor },
1044
1078
  { "ceiling", fn_ceiling },
1045
1079
  { "round", fn_round },
1080
+ #ifdef MKR_HOST_XML
1081
+ /* CSS-only internal hooks for untyped :*-of-type; the \x01-prefixed names are
1082
+ * unreachable from a user XPath expression (the lexer cannot produce them). */
1083
+ { MKR_FN_OF_TYPE_POS, fn_xml_of_type_pos },
1084
+ { MKR_FN_OF_TYPE_POS_LAST, fn_xml_of_type_pos_last },
1085
+ #endif
1046
1086
  { NULL, NULL },
1047
1087
  };
1048
1088
 
1049
- mkr_func_impl_t
1089
+ /* File-static: the function table is engine-internal. eval_fncall (later in the
1090
+ * merged engine TU) resolves through it; nothing outside the engine does. */
1091
+ static mkr_func_impl_t
1050
1092
  mkr_lookup_function(const char *ns_uri, const char *local_name)
1051
1093
  {
1052
1094
  if (local_name == NULL) return NULL;
1053
1095
  if (ns_uri != NULL) {
1054
1096
  /* Nokogiri-compatible builtins live in MKR_NS_NOKOGIRI_BUILTIN_URI.
1055
- * Other registered namespaces resolve to user-defined functions
1097
+ * Other registered namespaces resolve to user-defined functions -
1056
1098
  * not yet implemented (Phase 3 TODO: custom function registry). */
1057
1099
  if (strcmp(ns_uri, MKR_NS_NOKOGIRI_BUILTIN_URI) == 0) {
1058
1100
  for (size_t i = 0; fn_table_nokogiri_builtin[i].name; ++i) {
@@ -1063,7 +1105,7 @@ mkr_lookup_function(const char *ns_uri, const char *local_name)
1063
1105
  }
1064
1106
  return NULL;
1065
1107
  }
1066
- /* Default namespace XPath 1.0 standard library. */
1108
+ /* Default namespace - XPath 1.0 standard library. */
1067
1109
  for (size_t i = 0; fn_table[i].name; ++i) {
1068
1110
  if (strcmp(fn_table[i].name, local_name) == 0) return fn_table[i].fn;
1069
1111
  }