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
@@ -2,46 +2,20 @@
2
2
 
3
3
  #include <ctype.h>
4
4
  #include <stdint.h>
5
- #include <stdlib.h>
6
5
  #include <string.h>
7
6
 
8
7
  /*
9
8
  * XPath 1.0 lexer. The parser drives token consumption and handles
10
9
  * context-sensitive keywords (and, or, div, mod, *, node()...) via
11
- * lookahead the lexer is intentionally context-free.
10
+ * lookahead - the lexer is intentionally context-free.
11
+ *
12
+ * All input reads go through the bounded reader (core mkr_span_t): the input
13
+ * is a verified text (NUL-free, NUL-terminated, valid UTF-8) of known length,
14
+ * and the span makes an out-of-bounds read structurally impossible rather
15
+ * than a per-site convention. One-codepoint decoding is the strict core
16
+ * mkr_utf8_decode1 (the lexer formerly carried its own equivalent copy).
12
17
  */
13
18
 
14
- /* Decode one UTF-8 code point at NUL-terminated +p+. Returns its byte length
15
- * (1-4) and writes the code point to *cp; returns 0 at the terminator or on a
16
- * malformed/truncated/overlong/surrogate/out-of-range sequence (fail closed).
17
- * Never reads past the NUL: a NUL where a continuation byte is expected fails
18
- * the (b & 0xC0) == 0x80 check. */
19
- static int
20
- utf8_decode(const char *p, uint32_t *cp)
21
- {
22
- const unsigned char *u = (const unsigned char *)p;
23
- unsigned char b0 = u[0];
24
- if (b0 < 0x80) { *cp = b0; return b0 == 0 ? 0 : 1; }
25
-
26
- int n;
27
- uint32_t c, min;
28
- if ((b0 & 0xE0) == 0xC0) { n = 1; c = b0 & 0x1Fu; min = 0x80; }
29
- else if ((b0 & 0xF0) == 0xE0) { n = 2; c = b0 & 0x0Fu; min = 0x800; }
30
- else if ((b0 & 0xF8) == 0xF0) { n = 3; c = b0 & 0x07u; min = 0x10000; }
31
- else return 0; /* continuation byte or 0xF8+ as a lead: invalid */
32
-
33
- for (int i = 1; i <= n; i++) {
34
- unsigned char b = u[i];
35
- if ((b & 0xC0) != 0x80) return 0; /* truncated (incl. NUL) or bad continuation */
36
- c = (c << 6) | (b & 0x3Fu);
37
- }
38
- if (c < min) return 0; /* overlong */
39
- if (c >= 0xD800 && c <= 0xDFFF) return 0; /* surrogate */
40
- if (c > 0x10FFFF) return 0; /* out of Unicode range */
41
- *cp = c;
42
- return n + 1;
43
- }
44
-
45
19
  /* NCName code-point classes per XPath 1.0 §3.7 -> Namespaces in XML -> the
46
20
  * XML 1.0 (5th ed.) Name production, minus ':' (NCName). NameStartChar and
47
21
  * the extra NameChar ranges; this is the definition browsers / libxml2 track. */
@@ -67,109 +41,92 @@ is_ncname_cont_cp(uint32_t c)
67
41
  || (c >= 0x0300 && c <= 0x036F) || (c >= 0x203F && c <= 0x2040);
68
42
  }
69
43
 
70
- /* If +p+ begins an NCName character, return its byte length (1-4); else 0.
71
- * +start+ selects the NameStartChar (1) vs NameChar (0) class. */
44
+ /* If the span's cursor (+off bytes) begins an NCName character, return its
45
+ * byte length (1-4); else 0 (including at end-of-input - the strict decode
46
+ * fails closed there). +start+ selects NameStartChar (1) vs NameChar (0). */
72
47
  static int
73
- ncname_char(const char *p, int start)
48
+ ncname_char(const mkr_span_t *in, size_t off, int start)
74
49
  {
50
+ mkr_span_t s = mkr_span_tail(in, off);
75
51
  uint32_t cp;
76
- int len = utf8_decode(p, &cp);
52
+ int len = mkr_utf8_decode1_span(&s, &cp);
77
53
  if (len == 0) return 0;
78
54
  return (start ? is_ncname_start_cp(cp) : is_ncname_cont_cp(cp)) ? len : 0;
79
55
  }
80
56
 
81
- static void
82
- skip_ws(mkr_lexer_t *L)
83
- {
84
- /* XPath 1.0 §3.7 ExprWhitespace = XML S = (#x20 | #x9 | #xD | #xA)+ only.
85
- * Not C isspace(), which would also skip #xB (\v) and #xC (\f) — those are
86
- * not XPath whitespace and must surface as a syntax error. */
87
- char c;
88
- while ((c = *L->cur) == ' ' || c == '\t' || c == '\r' || c == '\n') L->cur++;
89
- }
90
-
91
57
  static int
92
58
  lex_number(mkr_lexer_t *L, mkr_token_t *t, mkr_xpath_error_t *err)
93
59
  {
94
- const char *s = L->cur;
95
- char *end = NULL;
96
- double v = strtod(s, &end);
97
- if (end == s) {
98
- mkr_err_setf(err, MKR_XPATH_ERR_SYNTAX, "expected number at '%.10s'", s);
60
+ /* Grammar-exact Number lex: the audited shared helpers scan and convert ONLY
61
+ * the bytes matching XPath 1.0 `Digits ('.' Digits?)? | '.' Digits`. We hand
62
+ * the helper a mark+length slice of the bounded reader (the sanctioned
63
+ * parser-TU pattern); it stays C-locale, so "0x1A" lexes as NUMBER 0 then NAME
64
+ * "x1A" and "1e3" as NUMBER 1 then NAME "e3", matching libxml2 / browsers. No
65
+ * raw scan (no strtod) remains in this TU. */
66
+ const char *mark = mkr_span_mark(&L->in);
67
+ size_t extent = mkr_xpath_number_extent(mark, mkr_span_left(&L->in));
68
+ if (extent == 0) {
69
+ mkr_err_setf(err, MKR_XPATH_ERR_SYNTAX, "expected number at '%.10s'", mark);
99
70
  return -1;
100
71
  }
101
- t->kind = MKR_TK_NUMBER;
102
- t->text.ptr = s;
103
- t->text.len = (size_t)(end - s);
104
- t->num = v;
105
- L->cur = end;
72
+ t->kind = MKR_TK_NUMBER;
73
+ t->text.ptr = mark;
74
+ t->text.len = extent;
75
+ t->num = mkr_xpath_number_from_extent(mark, extent);
76
+ mkr_span_skip(&L->in, extent);
106
77
  return 0;
107
78
  }
108
79
 
109
- /* Is [s, s+len) well-formed UTF-8? Strict (same rules as utf8_decode: rejects
110
- * malformed/overlong/surrogate/out-of-range). */
111
- static int
112
- utf8_valid_range(const char *s, size_t len)
113
- {
114
- const char *p = s, *e = s + len;
115
- while (p < e) {
116
- uint32_t cp;
117
- int n = utf8_decode(p, &cp);
118
- if (n <= 0 || (size_t)n > (size_t)(e - p)) return 0;
119
- p += n;
120
- }
121
- return 1;
122
- }
123
-
124
80
  static int
125
81
  lex_string(mkr_lexer_t *L, mkr_token_t *t, mkr_xpath_error_t *err)
126
82
  {
127
- char quote = *L->cur;
128
- const char *start = L->cur + 1;
129
- const char *p = start;
130
- while (*p && *p != quote) p++;
131
- if (*p != quote) {
83
+ int quote = mkr_span_peek(&L->in);
84
+ mkr_span_skip(&L->in, 1);
85
+ const char *start = mkr_span_mark(&L->in);
86
+ size_t len;
87
+ if (!mkr_span_find(&L->in, (char)quote, &len)) {
132
88
  mkr_err_set(err, MKR_XPATH_ERR_SYNTAX, "unterminated string literal");
133
89
  return -1;
134
90
  }
135
91
  /* Validate UTF-8 here, once, so every character-wise string function
136
92
  * (translate, substring, string-length, ...) can assume well-formed input
137
93
  * and so an invalid literal fails closed (SyntaxError) rather than producing
138
- * a silently wrong/truncated result. */
139
- size_t len = (size_t)(p - start);
140
- if (!utf8_valid_range(start, len)) {
94
+ * a silently wrong/truncated result. The strict core validator applies the
95
+ * same rules (overlong/surrogate/out-of-range rejected) as the strict
96
+ * decoder the rest of the lexer uses. */
97
+ if (!mkr_utf8_valid((const unsigned char *)start, len)) {
141
98
  mkr_err_set(err, MKR_XPATH_ERR_SYNTAX, "invalid UTF-8 in string literal");
142
99
  return -1;
143
100
  }
144
101
  t->kind = MKR_TK_LITERAL;
145
102
  t->text.ptr = start;
146
103
  t->text.len = len;
147
- L->cur = p + 1;
104
+ mkr_span_skip(&L->in, len + 1); /* content + closing quote */
148
105
  return 0;
149
106
  }
150
107
 
151
108
  static int
152
109
  lex_name(mkr_lexer_t *L, mkr_token_t *t)
153
110
  {
154
- const char *s = L->cur;
111
+ const char *s = mkr_span_mark(&L->in);
155
112
  int n;
156
- while ((n = ncname_char(L->cur, 0)) > 0) L->cur += n;
113
+ while ((n = ncname_char(&L->in, 0, 0)) > 0) mkr_span_skip(&L->in, (size_t)n);
157
114
  /* QName NameTest is `prefix:local` OR `prefix:*` (XPath 1.0 §2.3); the ':'
158
115
  * must not be the '::' axis separator. */
159
- if (*L->cur == ':' && L->cur[1] != ':'
160
- && (L->cur[1] == '*' || ncname_char(L->cur + 1, 1) > 0)) {
161
- L->cur++; /* eat ':' */
162
- if (*L->cur == '*') {
163
- L->cur++; /* prefix:* */
116
+ if (mkr_span_peek(&L->in) == ':' && mkr_span_at(&L->in, 1) != ':'
117
+ && (mkr_span_at(&L->in, 1) == '*' || ncname_char(&L->in, 1, 1) > 0)) {
118
+ mkr_span_skip(&L->in, 1); /* eat ':' */
119
+ if (mkr_span_peek(&L->in) == '*') {
120
+ mkr_span_skip(&L->in, 1); /* prefix:* */
164
121
  } else {
165
- while ((n = ncname_char(L->cur, 0)) > 0) L->cur += n;
122
+ while ((n = ncname_char(&L->in, 0, 0)) > 0) mkr_span_skip(&L->in, (size_t)n);
166
123
  }
167
124
  t->kind = MKR_TK_QNAME;
168
125
  } else {
169
126
  t->kind = MKR_TK_NAME;
170
127
  }
171
128
  t->text.ptr = s;
172
- t->text.len = (size_t)(L->cur - s);
129
+ t->text.len = mkr_span_since(&L->in, s);
173
130
  return 0;
174
131
  }
175
132
 
@@ -177,45 +134,46 @@ static int
177
134
  next_token(mkr_lexer_t *L, mkr_token_t *t, mkr_xpath_error_t *err)
178
135
  {
179
136
  memset(t, 0, sizeof(*t));
180
- skip_ws(L);
181
- const char *s = L->cur;
182
- char c = *s;
137
+ mkr_span_skip_xpath_ws(&L->in); /* XPath S only - see mkr_xpath_is_ws */
138
+ const char *s = mkr_span_mark(&L->in);
139
+ int c = mkr_span_peek(&L->in);
183
140
 
184
- if (c == '\0') {
141
+ if (c < 0) {
185
142
  t->kind = MKR_TK_EOF;
186
143
  t->text.ptr = s;
187
144
  t->text.len = 0;
188
145
  return 0;
189
146
  }
190
147
 
191
- if (c == '/' && s[1] == '/') { t->kind = MKR_TK_DSLASH; t->text.ptr = s; t->text.len = 2; L->cur += 2; return 0; }
192
- if (c == '.' && s[1] == '.') { t->kind = MKR_TK_DOTDOT; t->text.ptr = s; t->text.len = 2; L->cur += 2; return 0; }
193
- if (c == ':' && s[1] == ':') { t->kind = MKR_TK_COLONCOLON; t->text.ptr = s; t->text.len = 2; L->cur += 2; return 0; }
194
- if (c == '!' && s[1] == '=') { t->kind = MKR_TK_NE; t->text.ptr = s; t->text.len = 2; L->cur += 2; return 0; }
195
- if (c == '<' && s[1] == '=') { t->kind = MKR_TK_LE; t->text.ptr = s; t->text.len = 2; L->cur += 2; return 0; }
196
- if (c == '>' && s[1] == '=') { t->kind = MKR_TK_GE; t->text.ptr = s; t->text.len = 2; L->cur += 2; return 0; }
148
+ int c1 = mkr_span_at(&L->in, 1);
149
+ if (c == '/' && c1 == '/') { t->kind = MKR_TK_DSLASH; t->text.ptr = s; t->text.len = 2; mkr_span_skip(&L->in, 2); return 0; }
150
+ if (c == '.' && c1 == '.') { t->kind = MKR_TK_DOTDOT; t->text.ptr = s; t->text.len = 2; mkr_span_skip(&L->in, 2); return 0; }
151
+ if (c == ':' && c1 == ':') { t->kind = MKR_TK_COLONCOLON; t->text.ptr = s; t->text.len = 2; mkr_span_skip(&L->in, 2); return 0; }
152
+ if (c == '!' && c1 == '=') { t->kind = MKR_TK_NE; t->text.ptr = s; t->text.len = 2; mkr_span_skip(&L->in, 2); return 0; }
153
+ if (c == '<' && c1 == '=') { t->kind = MKR_TK_LE; t->text.ptr = s; t->text.len = 2; mkr_span_skip(&L->in, 2); return 0; }
154
+ if (c == '>' && c1 == '=') { t->kind = MKR_TK_GE; t->text.ptr = s; t->text.len = 2; mkr_span_skip(&L->in, 2); return 0; }
197
155
 
198
156
  switch (c) {
199
- case '(': t->kind = MKR_TK_LPAREN; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
200
- case ')': t->kind = MKR_TK_RPAREN; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
201
- case '[': t->kind = MKR_TK_LBRACKET; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
202
- case ']': t->kind = MKR_TK_RBRACKET; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
157
+ case '(': t->kind = MKR_TK_LPAREN; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
158
+ case ')': t->kind = MKR_TK_RPAREN; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
159
+ case '[': t->kind = MKR_TK_LBRACKET; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
160
+ case ']': t->kind = MKR_TK_RBRACKET; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
203
161
  case '.':
204
- if (isdigit((unsigned char)s[1])) {
162
+ if (c1 >= 0 && isdigit(c1)) {
205
163
  return lex_number(L, t, err);
206
164
  }
207
- t->kind = MKR_TK_DOT; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
208
- case '@': t->kind = MKR_TK_AT; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
209
- case ',': t->kind = MKR_TK_COMMA; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
210
- case '|': t->kind = MKR_TK_PIPE; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
211
- case '/': t->kind = MKR_TK_SLASH; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
212
- case '+': t->kind = MKR_TK_PLUS; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
213
- case '-': t->kind = MKR_TK_MINUS; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
214
- case '*': t->kind = MKR_TK_STAR; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
215
- case '=': t->kind = MKR_TK_EQ; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
216
- case '<': t->kind = MKR_TK_LT; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
217
- case '>': t->kind = MKR_TK_GT; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
218
- case '$': t->kind = MKR_TK_DOLLAR; t->text.ptr = s; t->text.len = 1; L->cur++; return 0;
165
+ t->kind = MKR_TK_DOT; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
166
+ case '@': t->kind = MKR_TK_AT; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
167
+ case ',': t->kind = MKR_TK_COMMA; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
168
+ case '|': t->kind = MKR_TK_PIPE; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
169
+ case '/': t->kind = MKR_TK_SLASH; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
170
+ case '+': t->kind = MKR_TK_PLUS; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
171
+ case '-': t->kind = MKR_TK_MINUS; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
172
+ case '*': t->kind = MKR_TK_STAR; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
173
+ case '=': t->kind = MKR_TK_EQ; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
174
+ case '<': t->kind = MKR_TK_LT; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
175
+ case '>': t->kind = MKR_TK_GT; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
176
+ case '$': t->kind = MKR_TK_DOLLAR; t->text.ptr = s; t->text.len = 1; mkr_span_skip(&L->in, 1); return 0;
219
177
  case '\'':
220
178
  case '"':
221
179
  return lex_string(L, t, err);
@@ -223,27 +181,25 @@ next_token(mkr_lexer_t *L, mkr_token_t *t, mkr_xpath_error_t *err)
223
181
  break;
224
182
  }
225
183
 
226
- if (isdigit((unsigned char)c)) {
184
+ if (isdigit(c)) {
227
185
  return lex_number(L, t, err);
228
186
  }
229
- if (ncname_char(s, 1) > 0) {
187
+ if (ncname_char(&L->in, 0, 1) > 0) {
230
188
  return lex_name(L, t);
231
189
  }
232
190
 
233
- unsigned char uc = (unsigned char)c;
234
- if (uc >= 0x20 && uc < 0x7F) {
191
+ if (c >= 0x20 && c < 0x7F) {
235
192
  mkr_err_setf(err, MKR_XPATH_ERR_SYNTAX, "unexpected character '%c'", c);
236
193
  } else {
237
- mkr_err_setf(err, MKR_XPATH_ERR_SYNTAX, "unexpected byte 0x%02x", uc);
194
+ mkr_err_setf(err, MKR_XPATH_ERR_SYNTAX, "unexpected byte 0x%02x", (unsigned)c);
238
195
  }
239
196
  return -1;
240
197
  }
241
198
 
242
199
  void
243
- mkr_lexer_init(mkr_lexer_t *L, const char *src, mkr_xpath_error_t *err)
200
+ mkr_lexer_init(mkr_lexer_t *L, const char *src, size_t len, mkr_xpath_error_t *err)
244
201
  {
245
- L->src = src;
246
- L->cur = src;
202
+ L->in = mkr_span(src, len);
247
203
  memset(&L->tok, 0, sizeof(L->tok));
248
204
  L->good = (next_token(L, &L->tok, err) == 0);
249
205
  }
@@ -255,6 +211,20 @@ mkr_lexer_advance(mkr_lexer_t *L, mkr_xpath_error_t *err)
255
211
  return L->good ? 0 : -1;
256
212
  }
257
213
 
214
+ /* The next non-whitespace byte after the current token (or -1 at the end),
215
+ * without consuming anything - the parser's function-call lookahead ("is this
216
+ * NAME followed by '('?"), kept here so the raw cursor never leaves the lexer. */
217
+ int
218
+ mkr_lexer_peek_nonws(const mkr_lexer_t *L)
219
+ {
220
+ mkr_span_t s = L->in;
221
+ for (;;) {
222
+ int c = mkr_span_peek(&s);
223
+ if (mkr_xpath_is_ws(c)) { mkr_span_skip(&s, 1); continue; }
224
+ return c;
225
+ }
226
+ }
227
+
258
228
  int
259
229
  mkr_tok_is_word_len(const mkr_token_t *t, const char *word, size_t word_len)
260
230
  {
@@ -0,0 +1,138 @@
1
+ /* mkr_xpath_node_access_html.h - node-access contract for the XPath engine.
2
+ *
3
+ * The engine's per-node work (navigation, name tests, namespace tests) is
4
+ * expressed through these MKR_NODE_* / MKR_ELEM_* macros instead of touching a
5
+ * concrete node struct directly. This lets the same engine logic be compiled
6
+ * for two node representations with ZERO runtime dispatch (compile-time
7
+ * monomorphization - measured ~0% vs direct field reads, whereas a runtime
8
+ * kind-branch costs ~+150%; see tmp/xml_spike/accessor_perf.c and
9
+ * docs/xml_parser_plan.ja.md §2.5):
10
+ *
11
+ * - HTML: this header, accessors → lxb_dom_node_t fields / Lexbor lookups.
12
+ * - XML (planned, §8.5): a sibling header binding the same macros to the
13
+ * custom mkr_xml_node_t, then `#include`-ing the shared engine body.
14
+ *
15
+ * For the HTML instance every macro expands to exactly the field read / call
16
+ * the engine used before, so the compiled object code (and `rake bench`) is
17
+ * unchanged by construction. Node-type numeric values are shared between the
18
+ * representations (the custom XML node reuses LXB_DOM_NODE_TYPE_* values), so
19
+ * the type switches in the engine stay representation-neutral.
20
+ */
21
+ #ifndef MKR_XPATH_NODE_ACCESS_HTML_H
22
+ #define MKR_XPATH_NODE_ACCESS_HTML_H
23
+
24
+ #include <lexbor/dom/dom.h>
25
+ #include <lexbor/ns/ns.h>
26
+
27
+ /* (The DOM node handle for this instance is lxb_dom_node_t; the XML instance
28
+ * will bind these macros to mkr_xml_node_t. No engine-wide typedef here - the
29
+ * engine already uses mkr_node_t for its XPath AST node.) */
30
+
31
+ /* --- neutral node-type constants ---
32
+ * The shared engine body compares MKR_NODE_TYPE(n) against these instead of a
33
+ * representation-specific name, so the body is representation-neutral. This is
34
+ * the per-instance binding: the HTML instance maps the neutral names to Lexbor's
35
+ * LXB_DOM_NODE_TYPE_*; the XML instance (mkr_xpath_node_access_xml.h) maps the
36
+ * same names to MKR_XML_NODE_TYPE_*. The two encodings are asserted equal there,
37
+ * so the integer in a node's `type` field means the same thing to either
38
+ * instance (byte-identical codegen for HTML).
39
+ *
40
+ * NB: named MKR_NTYPE_* (node *type*), distinct from the engine's existing
41
+ * mkr_nt_kind_t enum MKR_NT_* (node *test* kinds: text()/comment()/pi()/...).
42
+ * An earlier MKR_NT_* name collided with that enum and broke //comment() etc. */
43
+ #define MKR_NTYPE_ELEMENT LXB_DOM_NODE_TYPE_ELEMENT
44
+ #define MKR_NTYPE_ATTRIBUTE LXB_DOM_NODE_TYPE_ATTRIBUTE
45
+ #define MKR_NTYPE_TEXT LXB_DOM_NODE_TYPE_TEXT
46
+ #define MKR_NTYPE_CDATA_SECTION LXB_DOM_NODE_TYPE_CDATA_SECTION
47
+ #define MKR_NTYPE_COMMENT LXB_DOM_NODE_TYPE_COMMENT
48
+ #define MKR_NTYPE_PI LXB_DOM_NODE_TYPE_PROCESSING_INSTRUCTION
49
+ #define MKR_NTYPE_DOCUMENT_TYPE LXB_DOM_NODE_TYPE_DOCUMENT_TYPE
50
+ #define MKR_NTYPE_ENTITY LXB_DOM_NODE_TYPE_ENTITY
51
+ #define MKR_NTYPE_ENTITY_REFERENCE LXB_DOM_NODE_TYPE_ENTITY_REFERENCE
52
+ #define MKR_NTYPE_NOTATION LXB_DOM_NODE_TYPE_NOTATION
53
+
54
+ /* --- type / namespace-id (numeric, shared values) --- */
55
+ #define MKR_NODE_TYPE(n) ((n)->type)
56
+ #define MKR_NODE_NS_ID(n) ((n)->ns)
57
+
58
+ /* --- navigation (return the node handle for this instance) --- */
59
+ #define MKR_NODE_FIRST_CHILD(n) ((n)->first_child)
60
+ #define MKR_NODE_LAST_CHILD(n) ((n)->last_child)
61
+ #define MKR_NODE_NEXT(n) ((n)->next)
62
+ #define MKR_NODE_PREV(n) ((n)->prev)
63
+ #define MKR_NODE_PARENT(n) ((n)->parent)
64
+
65
+ /* --- element / attribute handles & iteration --- */
66
+ #define MKR_NODE_AS_ELEMENT(n) lxb_dom_interface_element(n) /* node -> element handle */
67
+ #define MKR_ELEM_FIRST_ATTR(el) ((el)->first_attr)
68
+ #define MKR_ATTR_NEXT(a) ((a)->next)
69
+ #define MKR_ATTR_VALUE(a, lenp) lxb_dom_attr_value((a), (lenp)) /* borrowed value bytes */
70
+
71
+ /* Strict element name tests resolve in the HTML namespace, so a foreign
72
+ * (non-HTML, non-null) element namespace is a non-match. The XML instance will
73
+ * redefine this against its own namespace representation. */
74
+ #define MKR_NODE_IS_FOREIGN_NS(n) \
75
+ ((n)->ns != LXB_NS_HTML && (n)->ns != LXB_NS__UNDEF)
76
+
77
+ /* --- name lookups (return borrowed lxb_char_t* bytes, set *(lenp)) --- */
78
+ #define MKR_ELEM_LOCAL_NAME(n, lenp) \
79
+ lxb_dom_element_local_name((lxb_dom_element_t *)(n), (lenp))
80
+ #define MKR_ATTR_LOCAL_NAME(n, lenp) \
81
+ lxb_dom_attr_local_name((lxb_dom_attr_t *)(n), (lenp))
82
+ /* Borrowed qualified name of any node: HTML elements report their lowercase
83
+ * local name (the lowercase-tag HTML data model the engine assumes, matching
84
+ * Makiri::Node#name); every other kind defers to lxb_dom_node_name. The XML
85
+ * binding returns the node's contiguous qname instead. */
86
+ static inline const lxb_char_t *
87
+ mkr_html_node_name_qualified(lxb_dom_node_t *node, size_t *len)
88
+ {
89
+ if (node == NULL) { if (len) *len = 0; return NULL; }
90
+ if (node->type == LXB_DOM_NODE_TYPE_ELEMENT) {
91
+ return lxb_dom_element_qualified_name(lxb_dom_interface_element(node), len);
92
+ }
93
+ return lxb_dom_node_name(node, len);
94
+ }
95
+ #define MKR_ELEM_QUALIFIED_NAME(n, lenp) \
96
+ mkr_html_node_name_qualified((n), (lenp))
97
+ #define MKR_ATTR_QUALIFIED_NAME(n, lenp) \
98
+ lxb_dom_attr_qualified_name((lxb_dom_attr_t *)(n), (lenp))
99
+ #define MKR_NODE_PI_NAME(n, lenp) \
100
+ lxb_dom_node_name((n), (lenp))
101
+
102
+ /* element attribute value by (raw) name; returns borrowed bytes or NULL. */
103
+ #define MKR_ELEM_GET_ATTRIBUTE(el, name, nlen, vlenp) \
104
+ lxb_dom_element_get_attribute((el), (const lxb_char_t *)(name), (nlen), (vlenp))
105
+
106
+ /* --- per-instance services (resolved against the concrete representation) ---
107
+ * The engine body uses these instead of calling Lexbor directly, so the XML
108
+ * instance can bind them to the custom node (which carries ns_uri / value
109
+ * directly and has no Lexbor ns hash / tag table). */
110
+
111
+ /* Borrowed namespace-URI bytes for a node (or NULL, *lenp=0, if it has none).
112
+ * HTML resolves the node's ns-id against the document's ns hash. */
113
+ static inline const char *
114
+ mkr_html_node_ns_uri(lxb_dom_node_t *node, lxb_dom_document_t *doc, size_t *lenp)
115
+ {
116
+ *lenp = 0;
117
+ if (node->ns == LXB_NS__UNDEF || doc == NULL || doc->ns == NULL) return NULL;
118
+ return (const char *)lxb_ns_by_id(doc->ns, node->ns, lenp);
119
+ }
120
+ #define MKR_NODE_NS_URI(node, doc, lenp) mkr_html_node_ns_uri((node), (doc), (lenp))
121
+
122
+ /* Append a node's own text content to a mkr_buf_t, setting `st` (mkr_status_t).
123
+ * HTML uses lxb_dom_node_text_content (allocates; freed after the append). */
124
+ #define MKR_NODE_APPEND_OWN_TEXT(node, buf, st) \
125
+ do { \
126
+ size_t mkr__tlen = 0; \
127
+ lxb_char_t *mkr__t = lxb_dom_node_text_content((node), &mkr__tlen); \
128
+ (st) = (mkr__t == NULL) ? MKR_OK : mkr_buf_append((buf), mkr__t, mkr__tlen); \
129
+ if (mkr__t) lxb_dom_document_destroy_text((node)->owner_document, mkr__t); \
130
+ } while (0)
131
+
132
+ /* Resolve a tag-name to a Lexbor tag id for the //tag element-index fast path.
133
+ * Only ever reached when the element index is present (HTML); for XML the index
134
+ * is NULL so the fast path bails before this, but it must still compile. */
135
+ #define MKR_DOC_TAG_ID_BY_NAME(doc, ptr, len) \
136
+ lxb_tag_id_by_name((doc)->tags, (const lxb_char_t *)(ptr), (len))
137
+
138
+ #endif /* MKR_XPATH_NODE_ACCESS_HTML_H */
@@ -0,0 +1,145 @@
1
+ /* mkr_xpath_node_access_xml.h - node-access contract bound to the custom XML node.
2
+ *
3
+ * The XML counterpart of mkr_xpath_node_access_html.h: it binds the MKR_NODE_* / MKR_ELEM_*
4
+ * field-access macros + per-instance services to mkr_xml_node_t, so the shared
5
+ * XPath engine body (mkr_xpath_*_body.h) compiles for XML with zero runtime
6
+ * dispatch (§2.5). MKR_HOST_XML selects the engine's XML host-policy branches
7
+ * (name-test matching, §8.6). The custom node carries its namespace URI and a
8
+ * contiguous "prefix:local" qname directly, so there is no ns hash / tag table.
9
+ */
10
+ #ifndef MKR_XPATH_NODE_ACCESS_XML_H
11
+ #define MKR_XPATH_NODE_ACCESS_XML_H
12
+
13
+ #include <lexbor/dom/dom.h> /* for the MKR_XML_NODE_TYPE_* == LXB asserts + lxb_char_t */
14
+ #include "../xml/mkr_xml_node.h"
15
+ #include "../core/mkr_span.h" /* mkr_bytes_eq / mkr_span_starts (this header is
16
+ * included before mkr_core.h in the XML engine TU) */
17
+ #include <string.h>
18
+
19
+ #define MKR_HOST_XML 1
20
+
21
+ /* Bind the engine's neutral node-type names to the XML representation's own
22
+ * constants (the html binding maps the same names to LXB_DOM_NODE_TYPE_*). */
23
+ #define MKR_NTYPE_ELEMENT MKR_XML_NODE_TYPE_ELEMENT
24
+ #define MKR_NTYPE_ATTRIBUTE MKR_XML_NODE_TYPE_ATTRIBUTE
25
+ #define MKR_NTYPE_TEXT MKR_XML_NODE_TYPE_TEXT
26
+ #define MKR_NTYPE_CDATA_SECTION MKR_XML_NODE_TYPE_CDATA_SECTION
27
+ #define MKR_NTYPE_COMMENT MKR_XML_NODE_TYPE_COMMENT
28
+ #define MKR_NTYPE_PI MKR_XML_NODE_TYPE_PI
29
+ #define MKR_NTYPE_DOCUMENT_TYPE MKR_XML_NODE_TYPE_DOCUMENT_TYPE
30
+ #define MKR_NTYPE_ENTITY MKR_XML_NODE_TYPE_ENTITY
31
+ #define MKR_NTYPE_ENTITY_REFERENCE MKR_XML_NODE_TYPE_ENTITY_REFERENCE
32
+ #define MKR_NTYPE_NOTATION MKR_XML_NODE_TYPE_NOTATION
33
+
34
+ /* The whole monomorphization rests on the two representations agreeing on the
35
+ * node-type encoding (so a node's `type` integer means the same thing whichever
36
+ * instance walks it). Enforce that agreement at compile time rather than by
37
+ * comment - the XML constants must equal the Lexbor ones. */
38
+ _Static_assert((int)MKR_XML_NODE_TYPE_ELEMENT == (int)LXB_DOM_NODE_TYPE_ELEMENT, "node-type encoding drift: ELEMENT");
39
+ _Static_assert((int)MKR_XML_NODE_TYPE_ATTRIBUTE == (int)LXB_DOM_NODE_TYPE_ATTRIBUTE, "node-type encoding drift: ATTRIBUTE");
40
+ _Static_assert((int)MKR_XML_NODE_TYPE_TEXT == (int)LXB_DOM_NODE_TYPE_TEXT, "node-type encoding drift: TEXT");
41
+ _Static_assert((int)MKR_XML_NODE_TYPE_CDATA_SECTION == (int)LXB_DOM_NODE_TYPE_CDATA_SECTION, "node-type encoding drift: CDATA_SECTION");
42
+ _Static_assert((int)MKR_XML_NODE_TYPE_ENTITY_REFERENCE == (int)LXB_DOM_NODE_TYPE_ENTITY_REFERENCE, "node-type encoding drift: ENTITY_REFERENCE");
43
+ _Static_assert((int)MKR_XML_NODE_TYPE_ENTITY == (int)LXB_DOM_NODE_TYPE_ENTITY, "node-type encoding drift: ENTITY");
44
+ _Static_assert((int)MKR_XML_NODE_TYPE_PI == (int)LXB_DOM_NODE_TYPE_PROCESSING_INSTRUCTION, "node-type encoding drift: PI");
45
+ _Static_assert((int)MKR_XML_NODE_TYPE_COMMENT == (int)LXB_DOM_NODE_TYPE_COMMENT, "node-type encoding drift: COMMENT");
46
+ _Static_assert((int)MKR_XML_NODE_TYPE_DOCUMENT == (int)LXB_DOM_NODE_TYPE_DOCUMENT, "node-type encoding drift: DOCUMENT");
47
+ _Static_assert((int)MKR_XML_NODE_TYPE_DOCUMENT_TYPE == (int)LXB_DOM_NODE_TYPE_DOCUMENT_TYPE, "node-type encoding drift: DOCUMENT_TYPE");
48
+ _Static_assert((int)MKR_XML_NODE_TYPE_NOTATION == (int)LXB_DOM_NODE_TYPE_NOTATION, "node-type encoding drift: NOTATION");
49
+
50
+ /* type / synthetic namespace-id (1 = has a namespace, 0 = none). */
51
+ #define MKR_NODE_TYPE(n) ((n)->type)
52
+ #define MKR_NODE_NS_ID(n) ((n)->ns_uri_len ? 1u : 0u)
53
+
54
+ /* navigation */
55
+ #define MKR_NODE_FIRST_CHILD(n) ((n)->first_child)
56
+ #define MKR_NODE_LAST_CHILD(n) ((n)->last_child)
57
+ #define MKR_NODE_NEXT(n) ((n)->next)
58
+ #define MKR_NODE_PREV(n) ((n)->prev)
59
+ #define MKR_NODE_PARENT(n) ((n)->parent)
60
+
61
+ /* element / attribute handles & iteration - the node IS its own element handle;
62
+ * attributes are a sibling-linked list off the element's `attrs`.
63
+ *
64
+ * Host policy (§8.6): a namespace declaration (xmlns / xmlns:*) is a NAMESPACE
65
+ * node in XPath 1.0, NOT an attribute, so it must not appear on the attribute
66
+ * axis (an unprefixed-wildcard attribute test, count of attributes, etc. ignore
67
+ * it). The reader still keeps it as a DOM attribute node (Node#attribute_nodes /
68
+ * Node#[] read el->attrs directly, in glue), matching DOM Level 2; only the
69
+ * XPath attribute iteration skips it. So the engine's MKR_ELEM_FIRST_ATTR /
70
+ * MKR_ATTR_NEXT advance past ns declarations, and every attribute-axis consumer
71
+ * (walk, predicates, doc-order) inherits that. The iterators take const and
72
+ * yield mutable (like strchr), so const callers (the doc-order comparator) and
73
+ * mutable ones share one implementation. */
74
+ #define MKR_NODE_AS_ELEMENT(n) (n)
75
+
76
+ static inline int
77
+ mkr_xml_attr_is_ns_decl(const mkr_xml_node_t *a)
78
+ {
79
+ mkr_span_t q = mkr_span(a->qname, a->qname_len);
80
+ return mkr_bytes_eq(a->qname, a->qname_len, "xmlns", 5) /* exact "xmlns" */
81
+ || mkr_span_starts(&q, "xmlns:", 6); /* "xmlns:*" prefix */
82
+ }
83
+ static inline mkr_xml_node_t *
84
+ mkr_xml_first_xpath_attr(const mkr_xml_node_t *el)
85
+ {
86
+ const mkr_xml_node_t *a = el->attrs;
87
+ while (a != NULL && mkr_xml_attr_is_ns_decl(a)) a = a->next;
88
+ return (mkr_xml_node_t *)a;
89
+ }
90
+ static inline mkr_xml_node_t *
91
+ mkr_xml_next_xpath_attr(const mkr_xml_node_t *a)
92
+ {
93
+ for (a = a->next; a != NULL && mkr_xml_attr_is_ns_decl(a); a = a->next) { }
94
+ return (mkr_xml_node_t *)a;
95
+ }
96
+ #define MKR_ELEM_FIRST_ATTR(el) mkr_xml_first_xpath_attr(el)
97
+ #define MKR_ATTR_NEXT(a) mkr_xml_next_xpath_attr(a)
98
+ #define MKR_ATTR_VALUE(a, lenp) (*(lenp) = (a)->value_len, (const lxb_char_t *)(a)->value)
99
+
100
+ /* Strict unprefixed element name tests must match a no-namespace node only, so a
101
+ * node with any namespace URI is "foreign" to an unprefixed test (§8.6). */
102
+ #define MKR_NODE_IS_FOREIGN_NS(n) ((n)->ns_uri_len != 0)
103
+
104
+ /* name lookups (borrowed bytes; the qname is the contiguous "prefix:local"). */
105
+ #define MKR_ELEM_LOCAL_NAME(n, lenp) (*(lenp) = (n)->local_len, (const lxb_char_t *)(n)->local)
106
+ #define MKR_ATTR_LOCAL_NAME(n, lenp) (*(lenp) = (n)->local_len, (const lxb_char_t *)(n)->local)
107
+ #define MKR_ELEM_QUALIFIED_NAME(n, lenp) (*(lenp) = (n)->qname_len, (const lxb_char_t *)(n)->qname)
108
+ #define MKR_ATTR_QUALIFIED_NAME(n, lenp) (*(lenp) = (n)->qname_len, (const lxb_char_t *)(n)->qname)
109
+ #define MKR_NODE_PI_NAME(n, lenp) (*(lenp) = (n)->local_len, (const lxb_char_t *)(n)->local)
110
+
111
+ /* element attribute value by (raw, qualified) name; borrowed bytes or NULL. */
112
+ static inline const lxb_char_t *
113
+ mkr_xml_node_get_attribute(mkr_xml_node_t *el, const char *name, size_t nlen, size_t *vlenp)
114
+ {
115
+ for (mkr_xml_node_t *a = el->attrs; a != NULL; a = a->next) {
116
+ if (mkr_xml_attr_is_ns_decl(a)) continue; /* xmlns is a namespace node, not @attr */
117
+ if (mkr_bytes_eq(a->qname, a->qname_len, name, nlen)) {
118
+ *vlenp = a->value_len;
119
+ return (const lxb_char_t *)a->value;
120
+ }
121
+ }
122
+ *vlenp = 0;
123
+ return NULL;
124
+ }
125
+ #define MKR_ELEM_GET_ATTRIBUTE(el, name, nlen, vlenp) \
126
+ mkr_xml_node_get_attribute((el), (const char *)(name), (nlen), (vlenp))
127
+
128
+ /* --- per-instance services (the custom node carries these directly) --- */
129
+
130
+ /* Namespace URI bytes (borrowed) or NULL (len 0) if the node is in no namespace.
131
+ * +doc+ is unused: the node holds the resolved URI. */
132
+ #define MKR_NODE_NS_URI(node, doc, lenp) \
133
+ (*(lenp) = (node)->ns_uri_len, (node)->ns_uri_len ? (node)->ns_uri : NULL)
134
+
135
+ /* Append a node's own text content (its value slice) to a mkr_buf_t. */
136
+ #define MKR_NODE_APPEND_OWN_TEXT(node, buf, st) \
137
+ (st) = ((node)->value_len \
138
+ ? mkr_buf_append((buf), (const lxb_char_t *)(node)->value, (node)->value_len) \
139
+ : MKR_OK)
140
+
141
+ /* No tag-id table on the XML document; the //tag element-index fast path is
142
+ * never enabled for XML (the element index is NULL), so this is unreachable. */
143
+ #define MKR_DOC_TAG_ID_BY_NAME(doc, ptr, len) LXB_TAG__UNDEF
144
+
145
+ #endif /* MKR_XPATH_NODE_ACCESS_XML_H */