leptris 1.9.314.0 → 1.9.317.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/Rakefile +1 -1
  4. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  5. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  6. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  9. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  10. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  11. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  12. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  13. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  14. data/lib/leptris/version.rb +1 -1
  15. data/lib/leptris/xml/descriptor.rb +10 -2
  16. data/lib/leptris/xml/ffi.rb +7 -0
  17. data/vendor-src/README.md +1 -1
  18. data/vendor-src/libleptris/CHANGELOG.md +70 -0
  19. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  20. data/vendor-src/libleptris/bindings/rust/descriptor/src/lib.rs +6 -0
  21. data/vendor-src/libleptris/bindings/rust/descriptor/tests/layout.rs +1 -0
  22. data/vendor-src/libleptris/cli/commands/xquery.c +16 -4
  23. data/vendor-src/libleptris/src/include/leptris/descriptor.h +8 -1
  24. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +46 -5
  25. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +106 -21
  26. data/vendor-src/libleptris/src/leptris/xpath/functions.c +20 -0
  27. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +145 -32
  28. data/vendor-src/libleptris/src/leptris/xpath/parser.c +48 -8
  29. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +19 -0
  30. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +16 -0
  31. data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
  32. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +141 -0
  33. data/vendor-src/libleptris/test/parser/test_whitespace_shape.cpp +153 -0
  34. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +6 -6
  35. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +64 -0
  36. data/vendor-src/libleptris/vcpkg.json +1 -1
  37. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb8126eafe8c4996a94eac86638223b042ad19562d410215d939bd00c9382a74
4
- data.tar.gz: e55d86cec4c5607b17b3d7915385a09955f5ad071401b99708612fa95299e249
3
+ metadata.gz: b745c01d0d7ec0b75d4fe3a0b994fc5f2a95cdd58b7e318eaee8c33ce8a005c6
4
+ data.tar.gz: 0e17a0313b9d7ff6fd20b4eed9df95398d35592f3f6c92ce49455439610f3ed2
5
5
  SHA512:
6
- metadata.gz: 35e631bde1b299ab4e5946b3dc3bae97e6dfea4d0a9ffd1bf3147cce8624e0cc252bd3019c32fdeb8900b2df49ef8304a9525de6c4c8f6a08ebe7e774a553665
7
- data.tar.gz: 8df2e9db29263f460f693436ac58dcc951870a1915f30046321f8c0a37a66ca602cad09dfecc8e1af6f4f3ef7e2a034b16c04948deebdfe0b2367879e9fe5d49
6
+ metadata.gz: 28a4c82d4ea33fbfc9844a3c663b269943972da95d9a2339b30aa8c0cd3bc6a40d71748610a4d63c9cb55e8c856e4375c744c40c0065b9d808b8627be5102924
7
+ data.tar.gz: 7ace6d1ab6118f7fe3308543b42e6ebe60f91b84ad84297aa88dd892d42e8f26ab1f98961bc785393386cd31d45a209a9e27b93b73042bc914fc4f809f0a58f3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ## [1.9.317.1] - 2026-10-08
2
+
3
+ ### Fixed
4
+
5
+ - **The published 1.9.317.0 gem predates PR #395**: a parallel
6
+ release race published 1.9.317.0 from the carrier-folds ride
7
+ (bd81bdf) before the `:unqualified` ns form merged — this patch
8
+ release carries the #1560 `ns: :unqualified` DSL (element rows
9
+ bind the unwritten spelling under both namespace-less and
10
+ default-xmlns documents; the serialize→re-parse→re-walk
11
+ round-trip agrees) and the #1565 nested-capture serialization
12
+ pin to the published artifact line. Main already carried both
13
+ (PR #395); only the gem lagged.
14
+
15
+ ## [1.9.317.0] - 2026-10-08
16
+
17
+ ### Added — engine sync (libleptris 1.9.315-1.9.317)
18
+
19
+ - **Unqualified-only ns form for child rows (1.9.317, #1560)**:
20
+ `ns: :unqualified` matches the UNWRITTEN spelling — an
21
+ unprefixed child binds under both a namespace-less document and
22
+ a default-`xmlns` document, and prefixed spellings never bind
23
+ (the `NS_ANY` superset lutaml-model#932 had to settle for). The
24
+ form keys on the spelling, not the URI, so
25
+ serialize→re-parse→re-walk round-trips agree: a default-ns child
26
+ serializes bare and still re-binds. Element-row only (attributes
27
+ have no unprefixed namespace by XML rules). `PLAN_NS_UNQUALIFIED
28
+ = 3` attached; all three shapes + the round-trip spec-pinned.
29
+
30
+ ### Fixed — engine sync (1.9.316)
31
+
32
+ - **Nested captures emit their own rows (#1565)**: a captured
33
+ child's rows resolve against the CAPTURING row's
34
+ `child_plan_index` plan, so a child bound by a nested row (the
35
+ `w:font`-in-`w:fonts` shape) serializes its own attribute and
36
+ child rows instead of silently vanishing — pinned through
37
+ `Descriptor#serialize`.
38
+
39
+ ### Engine sync (1.9.315)
40
+
41
+ - Carrier-by-pointer node identity for the higher-order argument
42
+ channel (QT3 fold-left-009/016): element members keep their node
43
+ pointer, `intersect` dedups by node, single-element results ride
44
+ the carrier. Engine-internal to the XQuery engine — the binding
45
+ contract is unchanged (suite carries it).
46
+
1
47
  ## [1.9.314.0] - 2026-10-07
2
48
 
3
49
  ### Changed — engine sync (libleptris 1.9.314)
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.314"
11
+ LIBLEPTRIS_VERSION = "1.9.317"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.314.0"
4
+ VERSION = "1.9.317.1"
5
5
  end
@@ -175,11 +175,15 @@ class Leptris::XML::Descriptor
175
175
  case ns
176
176
  when nil, :none then [Leptris::XML::FFI::PLAN_NS_NONE, nil]
177
177
  when :any then [Leptris::XML::FFI::PLAN_NS_ANY, nil]
178
+ when :unqualified
179
+ # libleptris 1.9.317 (#1560): the UNWRITTEN spelling.
180
+ [Leptris::XML::FFI::PLAN_NS_UNQUALIFIED, nil]
178
181
  when Hash
179
182
  [Leptris::XML::FFI::PLAN_NS_EXACT,
180
183
  anchor_string(anchors, ns.fetch(:exact))]
181
184
  else
182
- raise ArgumentError, "ns must be :none, :any, or {exact: uri}"
185
+ raise ArgumentError,
186
+ "ns must be :none, :any, :unqualified, or {exact: uri}"
183
187
  end
184
188
  ep[:ns_form] = ns_form
185
189
  ep[:ns_uri] = ns_uri
@@ -230,10 +234,14 @@ class Leptris::XML::Descriptor
230
234
  # defaults to NONE for backward compatibility. WILDCARD
231
235
  # rows (#1552) read an explicit :ns through pad0 (an
232
236
  # explicit :none filters no-namespace remainder alone;
233
- # unset stays the catch-all ANY default).
237
+ # unset stays the catch-all ANY default). :unqualified
238
+ # (libleptris 1.9.317, #1560) matches the unwritten
239
+ # spelling on element rows — element-row only (attributes
240
+ # have no unprefixed namespace by XML rules).
234
241
  cp[:ns_form] =
235
242
  case row[:ns]
236
243
  when :any then Leptris::XML::FFI::PLAN_NS_ANY
244
+ when :unqualified then Leptris::XML::FFI::PLAN_NS_UNQUALIFIED
237
245
  when Hash then Leptris::XML::FFI::PLAN_NS_EXACT
238
246
  else Leptris::XML::FFI::PLAN_NS_NONE
239
247
  end
@@ -380,6 +380,13 @@ module Leptris
380
380
  PLAN_NS_NONE = 0
381
381
  PLAN_NS_EXACT = 1
382
382
  PLAN_NS_ANY = 2
383
+ # libleptris 1.9.317 (#1560): matches the UNWRITTEN spelling
384
+ # on element rows — no written prefix, regardless of the
385
+ # effective namespace URI (binds under both a namespace-less
386
+ # and a default-xmlns document; prefixed spellings never
387
+ # bind). Attributes have no unprefixed namespace by XML
388
+ # rules, so the form is element-row only.
389
+ PLAN_NS_UNQUALIFIED = 3
383
390
 
384
391
  PLAN_VALUE_ELEMENT = 0
385
392
  PLAN_VALUE_SCALAR = 1
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.314
4
+ - libleptris v1.9.317
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,75 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.317] - 2026-10-07
4
+
5
+ ### Added
6
+
7
+ - **Descriptor: unqualified-only ns form for child rows (#1560,
8
+ PR #1574).** `LEPTRIS_PLAN_NS_UNQUALIFIED = 3` matches the WRITTEN
9
+ spelling — a child row binds children with no written prefix
10
+ regardless of the effective namespace URI, and refuses prefixed
11
+ spellings. An unprefixed child binds under both a namespace-less
12
+ document and a default-`xmlns` document (`NS_NONE` drops the
13
+ latter); a `p:child` never binds (the `NS_ANY` superset
14
+ lutaml-model#932 had to settle for). The serialize -> re-parse ->
15
+ re-walk round-trip agrees on all three shapes because the form
16
+ keys on the spelling, not the URI: the serializer emits prefix
17
+ declarations only, so a default-ns child serializes bare and
18
+ still re-binds. Rust descriptor-ABI mirror shipped in the same
19
+ wave (leptris-rs#4).
20
+
21
+
22
+
23
+ ## [1.9.316] - 2026-10-07
24
+
25
+ ### Fixed
26
+
27
+ - **Descriptor plan serializer: nested captures emit their own rows
28
+ (#1565, PR #1573).** A captured child's rows now resolve against the
29
+ CAPTURING row's `child_plan_index` plan instead of the plan that
30
+ walked it, so a child bound by a nested row (the `w:font`-in-
31
+ `w:fonts` shape) serializes its own attribute and child rows —
32
+ previously they resolved against the parent plan and silently
33
+ vanished. CONTENT rows inline their text at every nesting level.
34
+ The new nested-capture spec also exposed a 2 x 16B leak: the inner
35
+ walk's ns stamp was overwritten without freeing; fixed
36
+ free-before-overwrite (leaks gate clean).
37
+
38
+
39
+
40
+ ## [1.9.315] - 2026-10-07
41
+
42
+ ### Added
43
+
44
+ - **Carrier-by-pointer node identity — QT3 fold-left-009/016
45
+ adopted (PR #1571).** Element members riding the higher-order
46
+ function argument channel keep their NODE POINTER (the \x03P
47
+ carrier), so node identity survives a string-typed channel:
48
+ `$foundSoFar intersect $this` dedups by node (fold-left-009's
49
+ distinct-nodes-stable), and fold results decode to real nodes so
50
+ `$result/text()` steps (fold-left-016's css-selector interpreter).
51
+ The fold suite runs 16/16 — both long-pinned cases adopted.
52
+ Pointers are eval-safe: constructor documents anchor on the
53
+ source tree (the #691 discipline). Along the way, three language
54
+ gaps closed: typeswitch arms accept `element(id)` /
55
+ `element(class)` NAME arguments with case-bound variables binding
56
+ node operands as nodes; keyword-named constructors parse
57
+ (`<div/>`, `<mod/>` — HTML tag names colliding with XQuery
58
+ operators); and single-element results ride the carrier instead
59
+ of collapsing to their string value.
60
+
61
+ ### Fixed
62
+
63
+ - **Pointer carriers only for real ELEMENT members.** The item
64
+ collector handed \x03P carriers to synthetic attribute and
65
+ namespace nodes too — heap-owned by the carrying nodeset, so the
66
+ carrier dangled once it was freed (AddressSanitizer
67
+ heap-use-after-free via fn:subsequence over an attribute step).
68
+ Real elements stay document-anchored for the evaluation;
69
+ attributes and namespaces keep the markup snapshot.
70
+
71
+
72
+
3
73
  ## [1.9.314] - 2026-10-07
4
74
 
5
75
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.314
6
+ VERSION 1.9.317
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -49,6 +49,12 @@ pub type PlanNsForm = u8;
49
49
  pub const PLAN_NS_NONE: PlanNsForm = 0;
50
50
  pub const PLAN_NS_EXACT: PlanNsForm = 1;
51
51
  pub const PLAN_NS_ANY: PlanNsForm = 2;
52
+ /// #1560: matches the UNWRITTEN spelling — no written prefix,
53
+ /// regardless of the effective namespace URI. An unprefixed child
54
+ /// binds under both a namespace-less document and a default-xmlns
55
+ /// document; prefixed spellings never bind. Element rows only
56
+ /// (unprefixed attributes have no namespace by XML rules).
57
+ pub const PLAN_NS_UNQUALIFIED: PlanNsForm = 3;
52
58
 
53
59
  /// Result node kinds (the value tree a walk returns).
54
60
  pub type PlanValueKind = i32;
@@ -34,6 +34,7 @@ fn kind_and_flag_constants_match_engine_headers() {
34
34
  assert_eq!(PLAN_NS_NONE, 0);
35
35
  assert_eq!(PLAN_NS_EXACT, 1);
36
36
  assert_eq!(PLAN_NS_ANY, 2);
37
+ assert_eq!(PLAN_NS_UNQUALIFIED, 3);
37
38
  assert_eq!(PLAN_VALUE_ELEMENT, 0);
38
39
  assert_eq!(PLAN_VALUE_SCALAR, 1);
39
40
  assert_eq!(PLAN_VALUE_COLLECTION, 2);
@@ -119,16 +119,28 @@ static cli_result_t xquery_run(int argc, char** argv) {
119
119
  cli_error("XQuery evaluation failed");
120
120
  rc = CLI_ERROR_XPATH;
121
121
  } else if (leptris_xpath_result_type(r) == LEPTRIS_XPATH_NODESET) {
122
+ /* carrier-by-pointer (fold-left-009/016): synth members may
123
+ * carry a node pointer — render its markup. */
124
+ extern void* xpath_carrier_node_of(const char* s);
122
125
  size_t n = leptris_xpath_result_count(r);
123
126
  for (size_t k = 0; k < n; k++) {
124
127
  if (k) putchar(' ');
128
+ LeptrisElement el = NULL;
129
+ if (leptris_xpath_result_node_kind(r, k) ==
130
+ LEPTRIS_XPATH_NODE_ELEMENT) {
131
+ el = leptris_xpath_result_get(r, k);
132
+ } else {
133
+ const char* raw =
134
+ leptris_xpath_result_node_value(r, k);
135
+ if (raw && (unsigned char)raw[0] == 0x03 &&
136
+ raw[1] == 'P')
137
+ el = (LeptrisElement)xpath_carrier_node_of(raw);
138
+ }
125
139
  /* Constructed elements (#181) are real nodes now — print
126
140
  * their serialized markup, not the string-value (the
127
141
  * pre-node ctor spelling printed markup too). */
128
- if (leptris_xpath_result_node_kind(r, k) ==
129
- LEPTRIS_XPATH_NODE_ELEMENT) {
130
- char* m = leptris_element_serialize(
131
- leptris_xpath_result_get(r, k), NULL);
142
+ if (el) {
143
+ char* m = leptris_element_serialize(el, NULL);
132
144
  fputs(m ? m : "", stdout);
133
145
  leptris_free_string(m);
134
146
  } else {
@@ -96,7 +96,14 @@ typedef enum {
96
96
  typedef enum {
97
97
  LEPTRIS_PLAN_NS_NONE = 0, /* only elements with no namespace */
98
98
  LEPTRIS_PLAN_NS_EXACT = 1, /* resolved namespace URI equals ns_uri */
99
- LEPTRIS_PLAN_NS_ANY = 2
99
+ LEPTRIS_PLAN_NS_ANY = 2,
100
+ /* #1560: matches the UNWRITTEN spelling — children with no
101
+ * prefix, regardless of the effective namespace URI. An
102
+ * unprefixed child binds under both a namespace-less document
103
+ * and a default-xmlns document; prefixed spellings never bind.
104
+ * Unprefixed attributes have no namespace by XML rules, so
105
+ * this form only applies to element rows. */
106
+ LEPTRIS_PLAN_NS_UNQUALIFIED = 3
100
107
  } LeptrisPlanNsForm;
101
108
 
102
109
  typedef struct {
@@ -562,6 +562,11 @@ static int dp_ns_binds(LeptrisElement elem, uint8_t ns_form,
562
562
  case LEPTRIS_PLAN_NS_ANY:
563
563
  default:
564
564
  return 1;
565
+ case LEPTRIS_PLAN_NS_UNQUALIFIED:
566
+ /* #1560: the WRITTEN spelling — no prefix, any
567
+ * effective URI (default-xmlns elements included);
568
+ * prefixed spellings never bind. */
569
+ return prefix == NULL;
565
570
  }
566
571
  }
567
572
 
@@ -852,6 +857,11 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
852
857
  * back to the target plan's (NESTED matches with
853
858
  * the target's form, #1115). */
854
859
  if (row->ns_prefix) {
860
+ /* The inner walk's own #1551 stamp may already
861
+ * hold this pair — free before overwriting or
862
+ * the first strdup orphans. */
863
+ free(v->ns_prefix);
864
+ free(v->ns_uri);
855
865
  v->ns_prefix = dp_strdup(row->ns_prefix);
856
866
  v->ns_uri = dp_strdup(row->ns_uri ? row->ns_uri
857
867
  : target->ns_uri);
@@ -1343,7 +1353,8 @@ static int dp_emit_value(const struct leptris_plan_result* v,
1343
1353
 
1344
1354
  static int dp_emit_element(const struct leptris_plan_result* v,
1345
1355
  const char* name, const LeptrisPlan pool,
1346
- dp_sb* b, const dp_decls* decls, int is_root) {
1356
+ const dp_plan* kids_container, dp_sb* b,
1357
+ const dp_decls* decls, int is_root) {
1347
1358
  if (!dp_emit_element_open(v, name, b, decls, is_root)) return 0;
1348
1359
  const struct leptris_plan_result** kids = NULL;
1349
1360
  size_t nk = 0, cap = 0;
@@ -1360,11 +1371,14 @@ static int dp_emit_element(const struct leptris_plan_result* v,
1360
1371
  return 0;
1361
1372
  }
1362
1373
  qsort((void*)kids, nk, sizeof(*kids), dp_kid_pos_cmp);
1363
- const dp_plan* container =
1364
- &((const struct leptris_plan*)pool)->plans[v->container_plan];
1374
+ /* #1565: kid ROWS live in the plan that WALKED this element —
1375
+ * container_plan is the plan whose row CAPTURED it (the
1376
+ * parent), so a nested member resolved its kids' rows against
1377
+ * the parent's rows and a CONTENT row serialized as an
1378
+ * empty-named wrapper. */
1365
1379
  int ok = 1;
1366
1380
  for (size_t i = 0; i < nk && ok; i++)
1367
- ok = dp_emit_value(kids[i], container, pool, b, NULL, 0);
1381
+ ok = dp_emit_value(kids[i], kids_container, pool, b, NULL, 0);
1368
1382
  free((void*)kids);
1369
1383
  if (!ok) return 0;
1370
1384
  return dp_sb_puts(b, "</") && dp_sb_puts(b, name) &&
@@ -1377,6 +1391,16 @@ static int dp_emit_value(const struct leptris_plan_result* v,
1377
1391
  if (v->kind == LEPTRIS_PLAN_VALUE_RAW)
1378
1392
  return dp_sb_puts(b, v->str ? v->str : "");
1379
1393
  if (v->kind == LEPTRIS_PLAN_VALUE_ELEMENT) {
1394
+ /* #1565: a walk member captured by a CONTENT row is an
1395
+ * unnamed element — the row lookup must precede the name
1396
+ * resolution, or the empty wire name becomes the wrapper
1397
+ * (<w:item><>text</></w:item>). Content emits inline text
1398
+ * at every nesting level. */
1399
+ if (container && v->row_index != UINT32_MAX &&
1400
+ v->row_index < container->child_count &&
1401
+ container->child_plans[v->row_index].kind ==
1402
+ LEPTRIS_PLAN_KIND_CONTENT)
1403
+ return dp_sb_escape(b, v->str ? v->str : "", 0);
1380
1404
  /* The element's own binding was stamped at walk time; a
1381
1405
  * walk member without one falls back to its plan's. */
1382
1406
  char* name = NULL;
@@ -1397,7 +1421,24 @@ static int dp_emit_value(const struct leptris_plan_result* v,
1397
1421
  else
1398
1422
  name = dp_strdup(local ? local : "");
1399
1423
  if (!name) return 0;
1400
- int ok = dp_emit_element(v, name, pool, b, decls, is_root);
1424
+ /* Kids resolve their rows against the plan that WALKED this
1425
+ * element: the capturing row's child_plan_index when nested,
1426
+ * else container_plan (the walk root is its own plan). */
1427
+ const struct leptris_plan* poolp =
1428
+ (const struct leptris_plan*)pool;
1429
+ const dp_plan* kids_container =
1430
+ &poolp->plans[v->container_plan];
1431
+ if (container && v->row_index != UINT32_MAX &&
1432
+ v->row_index < container->child_count) {
1433
+ const leptris_child_plan* cap =
1434
+ &container->child_plans[v->row_index];
1435
+ if (cap->child_plan_index >= 0 &&
1436
+ (uint32_t)cap->child_plan_index < poolp->plan_count)
1437
+ kids_container =
1438
+ &poolp->plans[cap->child_plan_index];
1439
+ }
1440
+ int ok = dp_emit_element(v, name, pool, kids_container, b,
1441
+ decls, is_root);
1401
1442
  free(name);
1402
1443
  return ok;
1403
1444
  }
@@ -147,6 +147,8 @@ extern char* get_node_text(void* node);
147
147
  /* functions_ext31.c — #691 source-document anchoring (#181 ctor docs). */
148
148
  extern int xq_anchor_on_source(XPathContext* ctx,
149
149
  struct leptris_document* anchored);
150
+ /* carrier-by-pointer decode (fold-left-009/016). */
151
+ extern void* xpath_carrier_node_of(const char* s);
150
152
 
151
153
  /* #181: a materialized ctor node splices its MARKUP into a
152
154
  * parent's serialized content — get_node_text would yield only its
@@ -791,6 +793,7 @@ static int op_relational_cmp(XPathOperatorType op, double a, double b) {
791
793
  * `instance of` and XQuery typeswitch. */
792
794
  int xpath_result_matches_type(struct leptris_xpath_result* v,
793
795
  const char* base) {
796
+ extern const char* leptris_element_get_name(LeptrisElement e);
794
797
  int is_string_ty = strcmp(base, "xs:string") == 0 ||
795
798
  strcmp(base, "xs:anyURI") == 0 ||
796
799
  strncmp(base, "xs:date", 7) == 0 ||
@@ -801,7 +804,9 @@ int xpath_result_matches_type(struct leptris_xpath_result* v,
801
804
  strcmp(base, "node()") != 0 &&
802
805
  strcmp(base, "item()") != 0 &&
803
806
  strcmp(base, "element()") != 0 &&
807
+ strncmp(base, "element(", 8) != 0 &&
804
808
  strcmp(base, "attribute()") != 0 &&
809
+ strncmp(base, "attribute(", 10) != 0 &&
805
810
  strcmp(base, "text()") != 0 &&
806
811
  strcmp(base, "comment()") != 0 &&
807
812
  strcmp(base, "processing-instruction()") != 0;
@@ -825,6 +830,22 @@ int xpath_result_matches_type(struct leptris_xpath_result* v,
825
830
  if (!(tag >= 0 && tag <= 7)) return 0;
826
831
  } else if (strcmp(base, "element()") == 0) {
827
832
  if (tag != (int)LEPTRIS_NODE_ELEMENT) return 0;
833
+ } else if (strncmp(base, "element(", 8) == 0) {
834
+ /* ElementTest with a NAME: element(id) matches
835
+ * local-name "id" (fold-left-016 selector arms);
836
+ * element(*) is any element. */
837
+ size_t bl = strlen(base);
838
+ if (tag != (int)LEPTRIS_NODE_ELEMENT || bl < 10 ||
839
+ base[bl - 1] != ')')
840
+ return 0;
841
+ if (!(bl == 11 && base[9] == '*')) {
842
+ const char* en =
843
+ leptris_element_get_name((LeptrisElement)n);
844
+ size_t nl = bl - 9;
845
+ if (!en || strlen(en) != nl ||
846
+ strncmp(en, base + 8, nl) != 0)
847
+ return 0;
848
+ }
828
849
  } else if (strcmp(base, "attribute()") == 0) {
829
850
  if (tag != (int)LEPTRIS_NODE_ATTRIBUTE) return 0;
830
851
  } else if (strcmp(base, "text()") == 0) {
@@ -1889,9 +1910,18 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1889
1910
  if (sv && sv[0] == '\x03' && sv[1] == 'M' &&
1890
1911
  xpath_array_members_of(sv, &mem, &mn) && mn) {
1891
1912
  for (size_t j = 0; j < mn; j++) {
1892
- XPathTextNode* tn = synth_text(
1893
- mem[j], strlen(mem[j]));
1894
- if (tn) xpath_nodeset_add(one, tn);
1913
+ /* carrier-by-pointer: element members
1914
+ * decode to their NODE (identity —
1915
+ * fold-left-009/016). */
1916
+ void* cn = xpath_carrier_node_of(mem[j]);
1917
+ if (cn) {
1918
+ xpath_nodeset_add(one,
1919
+ (LeptrisNode*)cn);
1920
+ } else {
1921
+ XPathTextNode* tn = synth_text(
1922
+ mem[j], strlen(mem[j]));
1923
+ if (tn) xpath_nodeset_add(one, tn);
1924
+ }
1895
1925
  free(mem[j]);
1896
1926
  }
1897
1927
  free(mem);
@@ -1900,11 +1930,19 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1900
1930
  sv[1] == 'E') {
1901
1931
  free(sv);
1902
1932
  } else {
1903
- XPathTextNode* tn =
1904
- synth_text(sv ? sv : "",
1905
- sv ? strlen(sv) : 0);
1906
- free(sv);
1907
- if (tn) xpath_nodeset_add(one, tn);
1933
+ void* cn =
1934
+ xpath_carrier_node_of(sv ? sv : "");
1935
+ if (cn) {
1936
+ xpath_nodeset_add(one, (LeptrisNode*)cn);
1937
+ } else {
1938
+ XPathTextNode* tn =
1939
+ synth_text(sv ? sv : "",
1940
+ sv ? strlen(sv) : 0);
1941
+ free(sv);
1942
+ if (tn) xpath_nodeset_add(one, tn);
1943
+ sv = NULL;
1944
+ }
1945
+ if (sv) free(sv);
1908
1946
  }
1909
1947
  }
1910
1948
  XPathVariable* ex = xpath_variable_set_get(
@@ -2045,11 +2083,26 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2045
2083
  XPathNodeSet* one = xpath_nodeset_new();
2046
2084
  if (one) {
2047
2085
  one->owns_synthetic_text = 1;
2048
- const char* bindv =
2049
- marked ? marked : (vs_ ? vs_ : "");
2050
- XPathTextNode* tn = xpath_synth_text(
2051
- bindv, strlen(bindv));
2052
- if (tn) xpath_nodeset_add(one, tn);
2086
+ if (v->type == XPATH_RESULT_NODESET &&
2087
+ v->value.nodeset_value &&
2088
+ v->value.nodeset_value->count > 0) {
2089
+ /* Node operands bind AS NODES — the arm
2090
+ * body steps axes on them
2091
+ * (fold-left-016: $a/text()); a
2092
+ * string-value synth has no children. */
2093
+ for (size_t xi = 0;
2094
+ xi < v->value.nodeset_value->count;
2095
+ xi++)
2096
+ xpath_nodeset_add(
2097
+ one,
2098
+ v->value.nodeset_value->nodes[xi]);
2099
+ } else {
2100
+ const char* bindv =
2101
+ marked ? marked : (vs_ ? vs_ : "");
2102
+ XPathTextNode* tn = xpath_synth_text(
2103
+ bindv, strlen(bindv));
2104
+ if (tn) xpath_nodeset_add(one, tn);
2105
+ }
2053
2106
  XPathVariable* var = xpath_variable_set_add(
2054
2107
  vs, vn, XPATH_VAR_TYPE_NODE_SET);
2055
2108
  if (var)
@@ -4377,6 +4430,17 @@ struct leptris_xpath_result* xpath_call_function_item(
4377
4430
  * typed/untyped carrier marks the M members
4378
4431
  * now carry. */
4379
4432
  const char* mv = mem[j];
4433
+ void* cn = xpath_carrier_node_of(mv);
4434
+ if (cn) {
4435
+ /* Node members atomize to their string
4436
+ * value (XDM function conversion). */
4437
+ const char* sv =
4438
+ leptris_element_text((LeptrisElement)cn);
4439
+ kids[j].value =
4440
+ (char*)(sv ? sv : "");
4441
+ karr[j] = &kids[j];
4442
+ continue;
4443
+ }
4380
4444
  if (mv && mv[0] == '\x03' && mv[1] &&
4381
4445
  mv[1] != 'A' && mv[1] != 'M')
4382
4446
  mv += 2;
@@ -4408,8 +4472,16 @@ struct leptris_xpath_result* xpath_call_function_item(
4408
4472
  else if (argv[i] && argv[i][0] == '\x03' &&
4409
4473
  argv[i][1] == 'E')
4410
4474
  argn[i].value = "";
4411
- else
4412
- argn[i].value = argv[i];
4475
+ else {
4476
+ void* cn = xpath_carrier_node_of(argv[i]);
4477
+ if (cn) {
4478
+ const char* sv = leptris_element_text(
4479
+ (LeptrisElement)cn);
4480
+ argn[i].value = (char*)(sv ? sv : "");
4481
+ } else {
4482
+ argn[i].value = argv[i];
4483
+ }
4484
+ }
4413
4485
  }
4414
4486
  child_arr[i] = &argn[i];
4415
4487
  }
@@ -4476,17 +4548,30 @@ struct leptris_xpath_result* xpath_call_function_item(
4476
4548
  if (argv[ai][0] == '\x03' && argv[ai][1] == 'M' &&
4477
4549
  xpath_array_members_of(argv[ai], &mem, &mn) && mn) {
4478
4550
  for (size_t j = 0; j < mn; j++) {
4479
- XPathTextNode* tn =
4480
- synth_text(mem[j], strlen(mem[j]));
4481
- if (tn) xpath_nodeset_add(one, tn);
4551
+ /* carrier-by-pointer: element members decode to
4552
+ * their NODE (identity — fold-left-009/016);
4553
+ * borrowed, not synth. */
4554
+ void* cn = xpath_carrier_node_of(mem[j]);
4555
+ if (cn) {
4556
+ xpath_nodeset_add(one, (LeptrisNode*)cn);
4557
+ } else {
4558
+ XPathTextNode* tn =
4559
+ synth_text(mem[j], strlen(mem[j]));
4560
+ if (tn) xpath_nodeset_add(one, tn);
4561
+ }
4482
4562
  free(mem[j]);
4483
4563
  }
4484
4564
  free(mem);
4485
4565
  } else if (!(argv[ai][0] == '\x03' &&
4486
4566
  argv[ai][1] == 'E')) {
4487
- XPathTextNode* tn =
4488
- synth_text(argv[ai], strlen(argv[ai]));
4489
- if (tn) xpath_nodeset_add(one, tn);
4567
+ void* cn = xpath_carrier_node_of(argv[ai]);
4568
+ if (cn) {
4569
+ xpath_nodeset_add(one, (LeptrisNode*)cn);
4570
+ } else {
4571
+ XPathTextNode* tn =
4572
+ synth_text(argv[ai], strlen(argv[ai]));
4573
+ if (tn) xpath_nodeset_add(one, tn);
4574
+ }
4490
4575
  }
4491
4576
  /* "\x03E": the empty sequence binds NO nodes, so
4492
4577
  * empty($p) is true (fold zero accumulators). */
@@ -1120,6 +1120,26 @@ static size_t de_collect(struct leptris_xpath_result* r, DeItem* out,
1120
1120
  if (c[0] == '\x03' && c[1] == 'B') {
1121
1121
  out[i].kind = 3;
1122
1122
  out[i].num = c[2] == 't' ? 1 : 0;
1123
+ } else if (c[0] == '\x03' && c[1] == 'P') {
1124
+ /* carrier-by-pointer: decode to the NODE's
1125
+ * MARKUP so both sides compare as markup
1126
+ * strings (real elements collect as markup in
1127
+ * the element branch below); the markup compare
1128
+ * reparses into de_node_equal. */
1129
+ extern void* xpath_carrier_node_of(const char*);
1130
+ extern char* ctor_member_markup(void* node);
1131
+ void* cn = xpath_carrier_node_of(c);
1132
+ if (cn) {
1133
+ out[i].kind = 2;
1134
+ out[i].str = ctor_member_markup(cn);
1135
+ if (!out[i].str) {
1136
+ out[i].kind = 0;
1137
+ out[i].node = cn;
1138
+ }
1139
+ } else {
1140
+ out[i].kind = 2;
1141
+ out[i].borrow = c;
1142
+ }
1123
1143
  } else if (c[0] == '\x03' && c[1] == 'A') {
1124
1144
  /* attribute ctor carrier: kind 4 compares
1125
1145
  * name + value (K2-SeqDeepEqualFunc-25/31/32) */