leptris 1.9.241.0-ppc64le-linux → 1.9.248.0-ppc64le-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33da53f49f6293aac4ce2fe9dab4dfd9bd3266a57f70c6ed9d819845b27c9f88
4
- data.tar.gz: 658b592e9a05fb73459d1a24fe91dbe50ab7c9e7fcc07412770acdbd6811f30a
3
+ metadata.gz: 1a2df0df2539065be9cdf2c91d7434745bf220e36f8c4027fe762edb2d6055ad
4
+ data.tar.gz: 7ed7ab6072077c4c36bf6df133ed8567e8a1b231c47817cdd9048f86c88d85de
5
5
  SHA512:
6
- metadata.gz: c6fa8395951fed5d82e2f25161dd6834d19b127f20f9c85f2836d79a1c16d1441a3302f08788c693a1a05fdf333fc9dd651230a477c158e9817ab2b5940ecc22
7
- data.tar.gz: 8e109ca1f336a137a23a243074c9d55eeaf8813e53f76c29767fa29d08e36ec594a858485360f18c228447bd269d175d40d80734ba465e6eac53522480508760
6
+ metadata.gz: 0efbd8c06e49ab5462d3a7231737e4f316b00acca24683086bbdd999ac0b40518e7b3c5c82c413e73bbc84cb9c515e7106bdf959960df4a6630a4d28991ab659
7
+ data.tar.gz: f10dec7d51be8be655776a14b8186cdb579e3357c0125a10a7fb46e90e492354b4963259eda32226042d4955ffb69c44a17aadf3b8fde5b31d0c16cb6d84965f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.248.0] - 2026-09-26
9
+
10
+ ### Fixed
11
+
12
+ - Vendored libleptris 1.9.248 (riding 1.9.247 + 1.9.248):
13
+ - xpath: filtering a non-nodeset base treats the atom as a
14
+ one-item sequence — `(1)[1]` keeps the atom and `(1)[1] + 1`
15
+ is 2 (was an unconditional empty nodeset).
16
+ - html: the document arena span is sized from the input once
17
+ (#1218) — borrowed pointers stay in the int32 compact range,
18
+ removing ~776k overflow-table inserts on a 2.6 MB parse;
19
+ measured 31.4 -> 50.9 MB/s (+62%) on the table-rows corpus.
20
+ - Restores the naming lockstep: the previous release vendored
21
+ engine 1.9.246 under the version 1.9.242.0; version.rb now
22
+ matches the vendored engine semver again.
23
+
8
24
  ## [1.9.241.0] - 2026-09-25
9
25
 
10
26
  ### Fixed
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.241"
11
+ LIBLEPTRIS_VERSION = "1.9.248"
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.241.0"
4
+ VERSION = "1.9.248.0"
5
5
  end
data/lib/libleptris.so CHANGED
Binary file
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.241
4
+ - libleptris v1.9.248
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,66 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.248] - 2026-09-26
4
+
5
+ <!-- Edit this section with the actual release notes. -->
6
+ <!-- See https://keepachangelog.com for format guidance. -->
7
+
8
+ ### Changed
9
+
10
+ - (describe changes here)
11
+
12
+
13
+ ## [1.9.247] - 2026-09-26
14
+
15
+ ### Fixed
16
+
17
+ - filter on a non-nodeset base treats the atom as a one-item sequence (xpath)
18
+
19
+ ## [1.9.246] - 2026-09-25
20
+
21
+ ### Added
22
+
23
+ - exact decimal lexical carrier (xquery)
24
+
25
+
26
+
27
+ ## [1.9.245] - 2026-09-25
28
+
29
+ ### Added
30
+
31
+ - array:remove + NaN subsequence 2-arg adoption (xquery)
32
+ - attribute ctor carriers + derived-string deep-equal class (xquery)
33
+
34
+
35
+
36
+ ## [1.9.244] - 2026-09-25
37
+
38
+ <!-- Edit this section with the actual release notes. -->
39
+ <!-- See https://keepachangelog.com for format guidance. -->
40
+
41
+ ### Changed
42
+
43
+ - (describe changes here)
44
+
45
+
46
+ ## [1.9.243] - 2026-09-25
47
+
48
+ <!-- Edit this section with the actual release notes. -->
49
+ <!-- See https://keepachangelog.com for format guidance. -->
50
+
51
+ ### Changed
52
+
53
+ - (describe changes here)
54
+
55
+
56
+ ## [1.9.242] - 2026-09-25
57
+
58
+ ### Added
59
+
60
+ - QT3 maps/arrays slice — empty-aggregate arity guard + canonical map order (xquery)
61
+
62
+
63
+
3
64
  ## [1.9.241] - 2026-09-25
4
65
 
5
66
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.241
6
+ VERSION 1.9.248
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -2290,6 +2290,23 @@ int leptris_il_scan(char* s, size_t len, int drop_ws, IlCtx* c,
2290
2290
  j = (size_t)(vend - s) + 1;
2291
2291
  IlAttr a = { (uint32_t)a0, (uint32_t)al,
2292
2292
  (uint32_t)v0, (uint32_t)vl, aws };
2293
+ /* XML 1.0 §3.1: duplicate attributes are a fatal error —
2294
+ * the classic lane reports "Attribute %s redefined" and
2295
+ * the SAX callback path records an ERROR event, but the
2296
+ * record table has no error channel. A dup-containing
2297
+ * document is therefore outside the scannable subset:
2298
+ * bail so callers fall back to the callback path (#281,
2299
+ * leptris-ruby#331 adoption). Same-element prior attrs
2300
+ * are the contiguous range [r.first_attr, nattr). */
2301
+ if (acount > 0) {
2302
+ for (uint32_t k = r.first_attr; k < (uint32_t)c->nattr;
2303
+ k++) {
2304
+ const IlAttr* p = &c->attrs[k];
2305
+ if (p->name_len == (uint32_t)al &&
2306
+ memcmp(s + p->name_off, s + a0, al) == 0)
2307
+ return 0;
2308
+ }
2309
+ }
2293
2310
  if (!il_push_attr(c, &a)) return 0;
2294
2311
  if (acount == 0) r.first_attr = (uint32_t)(c->nattr - 1);
2295
2312
  acount++;
@@ -5453,8 +5453,17 @@ static LeptrisDocument html_parse_shared(
5453
5453
  if (status) *status = LEPTRIS_ERROR_NULL_ARG;
5454
5454
  return NULL;
5455
5455
  }
5456
+ /* #1218: size the arena span from the input up front — the
5457
+ * HTML tree expands ~8x (nodes + attributes + the owned input
5458
+ * copy), and an undersized span spills later allocations into
5459
+ * extension blocks in a different malloc region, pushing every
5460
+ * borrowed pointer out of the int32 compact range (measured
5461
+ * 776k overflow-table inserts on the 2.6 MB table shape). */
5462
+ extern struct leptris_document* leptris_document_create_with_arena_size(
5463
+ size_t arena_size);
5456
5464
  struct leptris_document* doc =
5457
- (struct leptris_document*)leptris_document_create();
5465
+ (struct leptris_document*)leptris_document_create_with_arena_size(
5466
+ len * 10 + (1u << 18));
5458
5467
  if (!doc) {
5459
5468
  if (status) *status = LEPTRIS_ERROR_MEMORY;
5460
5469
  return NULL;
@@ -173,7 +173,17 @@ void leptris_parse_options_init(leptris_parse_options* opts) {
173
173
  * pool-allocated (TODO 154) zeroed struct, strict mode inherited
174
174
  * from the thread-local setting, standalone unset (-1).
175
175
  */
176
- LEPTRIS_API LeptrisDocument leptris_document_create(void) {
176
+ /* #1218: parse entry points that know their input size reserve the
177
+ * arena span ONCE at content-derived scale — an undersized span
178
+ * overflows into tracked extension blocks, which malloc places in a
179
+ * different region (measured 105 TB away on macOS, ~5.5 GB from the
180
+ * span). Every borrowed pointer (text runs, element names, attribute
181
+ * strings) then exceeds the int32 compact range and pays an
182
+ * overflow-table insert per field plus a hash lookup per decode for
183
+ * the REST of the parse. Sized spans keep the whole parse in one
184
+ * contiguous region. */
185
+ struct leptris_document* leptris_document_create_with_arena_size(
186
+ size_t arena_size) {
177
187
  extern LeptrisMemoryPool* leptris_pool_create_arena_backed(
178
188
  LeptrisArena*, int);
179
189
  extern void* leptris_pool_alloc(LeptrisMemoryPool* pool, size_t size);
@@ -185,7 +195,7 @@ LEPTRIS_API LeptrisDocument leptris_document_create(void) {
185
195
  * documents take the same arena-backed pool the parser uses
186
196
  * (overflow extends via tracked blocks; the pool's page
187
197
  * machinery no longer runs for documents). */
188
- LeptrisArena* arena = leptris_arena_create(64 * 1024);
198
+ LeptrisArena* arena = leptris_arena_create(arena_size);
189
199
  if (!arena) return NULL;
190
200
  LeptrisMemoryPool* pool = leptris_pool_create_arena_backed(arena, 1);
191
201
  if (!pool) {
@@ -210,6 +220,10 @@ LEPTRIS_API LeptrisDocument leptris_document_create(void) {
210
220
  return doc;
211
221
  }
212
222
 
223
+ LEPTRIS_API LeptrisDocument leptris_document_create(void) {
224
+ return leptris_document_create_with_arena_size(64 * 1024);
225
+ }
226
+
213
227
  LEPTRIS_API LeptrisDocument leptris_document_create_html(void) {
214
228
  struct leptris_document* doc = leptris_document_create();
215
229
  if (doc) doc->html_mode = 1;
@@ -659,6 +659,12 @@ struct leptris_xpath_result {
659
659
  * round-trip E form instead of the libxml2-parity XPath
660
660
  * spelling. Cleared in result_new (free-list recycle). */
661
661
  int xq_spelling;
662
+ /* Canonical decimal lexical: set by the xs:decimal ctor — the
663
+ * double carrier cannot hold 30 significant digits, so
664
+ * eq-based functions (distinct-values, index-of, deep-equal)
665
+ * key on the canonical form instead (decimal-lex slice). OWNED
666
+ * — freed in result_free, cleared in result_new (recycle). */
667
+ char* decimal_lex;
662
668
  };
663
669
 
664
670
  /* Namespace mapping for XPath context (v0.8.0) */
@@ -744,6 +744,7 @@ struct leptris_xpath_result* xpath_result_new(XPathResultType type) {
744
744
  result->is_int = 0; /* free-list recycling would leak it */
745
745
  result->atomic_type = NULL;
746
746
  result->xq_spelling = 0;
747
+ result->decimal_lex = NULL; /* free-list recycling would leak it */
747
748
 
748
749
  /* Initialize union based on type. XPATH_RESULT_CACHED is the
749
750
  * free-list sentinel — a fresh result never carries it. */
@@ -773,6 +774,11 @@ void xpath_result_free(struct leptris_xpath_result* result) {
773
774
  * no-op, never a second payload release. */
774
775
  if (result->type == XPATH_RESULT_CACHED) return;
775
776
 
777
+ if (result->decimal_lex) {
778
+ LEPTRIS_FREE(result->decimal_lex);
779
+ result->decimal_lex = NULL;
780
+ }
781
+
776
782
  /* Release the payload through locals: after this block the
777
783
  * union slot is dead, and parking the free-list next-pointer
778
784
  * into it cannot alias the memory just released. */
@@ -979,8 +985,48 @@ struct leptris_xpath_result* evaluate_expr(XPathContext* ctx, XPathASTNode* ast)
979
985
  if (!base) return NULL;
980
986
  if (base->type != XPATH_RESULT_NODESET ||
981
987
  !base->value.nodeset_value) {
988
+ /* Non-nodeset base: the atom is a one-item sequence
989
+ * — (1)[1] keeps it, (1)[true()] keeps it, (1)[2]
990
+ * empties (the old code returned empty
991
+ * unconditionally). The predicate sees position 1
992
+ * of 1; the context node stays the outer one. */
993
+ int keep = 1;
994
+ for (size_t p = 1; p < ast->child_count && keep; p++) {
995
+ XPathASTNode* pred = ast->children[p];
996
+ if (pred->type == XPATH_AST_NUMBER) {
997
+ keep = pred->number_value == 1.0;
998
+ continue;
999
+ }
1000
+ LeptrisElement old_node = ctx->context_node;
1001
+ size_t old_pos = ctx->context_position;
1002
+ size_t old_size = ctx->context_size;
1003
+ void* old_pred = ctx->current_predicate_node;
1004
+ ctx->context_position = 1;
1005
+ ctx->context_size = 1;
1006
+ struct leptris_xpath_result* pr =
1007
+ evaluate_expr(ctx, pred);
1008
+ ctx->context_node = old_node;
1009
+ ctx->context_position = old_pos;
1010
+ ctx->context_size = old_size;
1011
+ ctx->current_predicate_node = old_pred;
1012
+ if (!pr) {
1013
+ keep = 0;
1014
+ continue;
1015
+ }
1016
+ if (pr->type == XPATH_RESULT_NUMBER)
1017
+ keep =
1018
+ (size_t)pr->value.number_value == 1;
1019
+ else
1020
+ keep = xpath_to_boolean(pr) ? 1 : 0;
1021
+ xpath_result_free(pr);
1022
+ }
1023
+ if (keep) return base;
982
1024
  xpath_result_free(base);
983
- return xpath_result_new(XPATH_RESULT_NODESET);
1025
+ struct leptris_xpath_result* empty =
1026
+ xpath_result_new(XPATH_RESULT_NODESET);
1027
+ if (empty && empty->value.nodeset_value)
1028
+ empty->value.nodeset_value->is_sequence = 1;
1029
+ return empty;
984
1030
  }
985
1031
  if (ast->child_count > 1) {
986
1032
  apply_predicates(ctx, base->value.nodeset_value,
@@ -418,7 +418,9 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
418
418
  op0 != XPATH_OP_ELEMENT_CTOR &&
419
419
  op0 != XPATH_OP_ATTRIBUTE_CTOR &&
420
420
  op0 != XPATH_OP_TEXT_CTOR &&
421
- op0 != XPATH_OP_DOCUMENT_CTOR)
421
+ op0 != XPATH_OP_DOCUMENT_CTOR &&
422
+ op0 != XPATH_OP_ARRAY_CONSTRUCTOR &&
423
+ op0 != XPATH_OP_MAP_CONSTRUCTOR)
422
424
  return NULL;
423
425
 
424
426
  XPathOperatorType op = (XPathOperatorType)ast->number_value;
@@ -1597,6 +1599,38 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1597
1599
  if (v) xpath_result_free(v);
1598
1600
  }
1599
1601
  if (!s) s = leptris_strdup("");
1602
+ if (op == XPATH_OP_ATTRIBUTE_CTOR) {
1603
+ /* Top-level attribute ctor keeps its name: one
1604
+ * synthetic member carrying "\x03A" name "\x01"
1605
+ * value — deep-equal compares name+value
1606
+ * (K2-SeqDeepEqualFunc-25/31/32), string consumers
1607
+ * strip to the value. \x01 cannot occur in XML
1608
+ * content. */
1609
+ const char* an = ast->value ? ast->value : "";
1610
+ size_t alen = strlen(an), vlen = strlen(s);
1611
+ size_t clen = alen + vlen + 3;
1612
+ char* carrier = (char*)malloc(clen + 1);
1613
+ if (!carrier) { free(s); return NULL; }
1614
+ carrier[0] = '\x03';
1615
+ carrier[1] = 'A';
1616
+ memcpy(carrier + 2, an, alen);
1617
+ carrier[2 + alen] = '\x01';
1618
+ memcpy(carrier + 3 + alen, s, vlen + 1);
1619
+ free(s);
1620
+ XPathNodeSet* ns = xpath_nodeset_new();
1621
+ if (!ns) { free(carrier); return NULL; }
1622
+ ns->owns_synthetic_text = 1;
1623
+ ns->is_sequence = 1;
1624
+ XPathTextNode* tn = synth_text(carrier, clen);
1625
+ free(carrier);
1626
+ if (!tn) { xpath_nodeset_free(ns); return NULL; }
1627
+ xpath_nodeset_add(ns, tn);
1628
+ struct leptris_xpath_result* out =
1629
+ xpath_result_new(XPATH_RESULT_NODESET);
1630
+ if (!out) { xpath_nodeset_free(ns); return NULL; }
1631
+ out->value.nodeset_value = ns;
1632
+ return out;
1633
+ }
1600
1634
  struct leptris_xpath_result* out =
1601
1635
  xpath_result_new(XPATH_RESULT_STRING);
1602
1636
  if (!out) { free(s); return NULL; }
@@ -1903,6 +1937,81 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1903
1937
  free(v);
1904
1938
  }
1905
1939
  buf[len] = '\0';
1940
+ /* Canonical entry order: sort by key so deep-equal and the
1941
+ * serialized carrier are key-order-insensitive (maps-4:
1942
+ * {1:a, 2:b} = {2:b, 1:a}); map iteration order is
1943
+ * implementation-defined in XQuery. */
1944
+ {
1945
+ size_t body_len = len - 4; /* past "\x03MAP" */
1946
+ char* body = buf + 4;
1947
+ size_t count = 0;
1948
+ for (size_t i = 0; i < body_len; i++)
1949
+ if (body[i] == '\x02') count++;
1950
+ if (count > 1) {
1951
+ char** keys = (char**)malloc(count * sizeof(char*));
1952
+ size_t* klens = (size_t*)malloc(count * sizeof(size_t));
1953
+ char** vals = (char**)malloc(count * sizeof(char*));
1954
+ size_t* vlens = (size_t*)malloc(count * sizeof(size_t));
1955
+ if (keys && klens && vals && vlens) {
1956
+ size_t n = 0, p = 0;
1957
+ while (p < body_len) {
1958
+ if (body[p] != '\x02') { p++; continue; }
1959
+ p++;
1960
+ size_t ks = p;
1961
+ while (p < body_len && body[p] != '\x01') p++;
1962
+ keys[n] = body + ks;
1963
+ klens[n] = p - ks;
1964
+ p++;
1965
+ size_t vs = p;
1966
+ while (p < body_len && body[p] != '\x02') p++;
1967
+ vals[n] = body + vs;
1968
+ vlens[n] = p - vs;
1969
+ n++;
1970
+ }
1971
+ for (size_t a = 1; a < n; a++) {
1972
+ char* ek = keys[a]; size_t ekl = klens[a];
1973
+ char* ev = vals[a]; size_t evl = vlens[a];
1974
+ size_t b2 = a;
1975
+ while (b2 > 0) {
1976
+ int c = strncmp(keys[b2 - 1], ek,
1977
+ klens[b2 - 1] < ekl
1978
+ ? klens[b2 - 1] : ekl);
1979
+ if (c == 0)
1980
+ c = klens[b2 - 1] < ekl ? -1
1981
+ : klens[b2 - 1] > ekl ? 1 : 0;
1982
+ if (c <= 0) break;
1983
+ keys[b2] = keys[b2 - 1];
1984
+ klens[b2] = klens[b2 - 1];
1985
+ vals[b2] = vals[b2 - 1];
1986
+ vlens[b2] = vlens[b2 - 1];
1987
+ b2--;
1988
+ }
1989
+ keys[b2] = ek; klens[b2] = ekl;
1990
+ vals[b2] = ev; vlens[b2] = evl;
1991
+ }
1992
+ /* Sort-order permutes segments in place: write
1993
+ * through a scratch (a forward write clobbers a
1994
+ * not-yet-copied source segment). */
1995
+ char* scratch = (char*)malloc(body_len + 1);
1996
+ if (scratch) {
1997
+ size_t w2 = 0;
1998
+ for (size_t e = 0; e < n; e++) {
1999
+ scratch[w2++] = '\x02';
2000
+ memcpy(scratch + w2, keys[e], klens[e]);
2001
+ w2 += klens[e];
2002
+ scratch[w2++] = '\x01';
2003
+ memcpy(scratch + w2, vals[e], vlens[e]);
2004
+ w2 += vlens[e];
2005
+ }
2006
+ memcpy(body, scratch, w2);
2007
+ len = 4 + w2;
2008
+ buf[len] = '\0';
2009
+ free(scratch);
2010
+ }
2011
+ }
2012
+ free(keys); free(klens); free(vals); free(vlens);
2013
+ }
2014
+ }
1906
2015
  XPathNodeSet* out = xpath_nodeset_new();
1907
2016
  if (!out) { free(buf); return NULL; }
1908
2017
  out->owns_synthetic_text = 1;
@@ -2319,12 +2428,17 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2319
2428
  is->owns_synthetic_text = 0;
2320
2429
  } else {
2321
2430
  char* piece =
2322
- (ctx->xquery_spelling &&
2323
- item->type == XPATH_RESULT_NUMBER &&
2324
- !item->is_int)
2325
- ? xpath_number_to_string_xq(
2326
- item->value.number_value)
2327
- : xpath_to_string(item);
2431
+ item->decimal_lex
2432
+ ? leptris_strdup(item->decimal_lex)
2433
+ : (ctx->xquery_spelling &&
2434
+ item->type == XPATH_RESULT_NUMBER &&
2435
+ !item->is_int)
2436
+ ? xpath_number_to_string_xq_typed(
2437
+ item->value.number_value,
2438
+ item->atomic_type &&
2439
+ strcmp(item->atomic_type,
2440
+ "xs:float") == 0)
2441
+ : xpath_to_string(item);
2328
2442
  if (item->type == XPATH_RESULT_NUMBER) {
2329
2443
  /* "\x03N" marks numeric members for per-member
2330
2444
  * type checks (instance of); get_node_text
@@ -276,11 +276,18 @@ char* get_node_text(void* node) {
276
276
  * per-member type checks (instance of). \x03 is invalid
277
277
  * in XML 1.0 text, so it can never collide with real
278
278
  * content — strip it for every string consumer. */
279
- if (c[0] == '\x03' &&
280
- (c[1] == 'N' || c[1] == 'B' || c[1] == 'D' ||
281
- (c[1] == 'F' &&
282
- !((c[2] == 'N' && c[3] == '\x02') || c[2] == 'R'))))
283
- c += 2;
279
+ if (c[0] == '\x03') {
280
+ if (c[1] == 'A') {
281
+ /* attribute ctor carrier: string value is the
282
+ * part after the "\x01" separator */
283
+ const char* sep = strchr(c, '\x01');
284
+ return leptris_strdup(sep ? sep + 1 : "");
285
+ }
286
+ if (c[1] == 'N' || c[1] == 'B' || c[1] == 'D' ||
287
+ (c[1] == 'F' &&
288
+ !((c[2] == 'N' && c[3] == '\x02') || c[2] == 'R')))
289
+ c += 2;
290
+ }
284
291
  return leptris_strdup(c);
285
292
  }
286
293
 
@@ -299,8 +306,30 @@ int xpath_to_boolean(struct leptris_xpath_result* result) {
299
306
  return result->value.number_value != 0.0 && !isnan(result->value.number_value);
300
307
  case XPATH_RESULT_STRING:
301
308
  return result->value.string_value && result->value.string_value[0] != '\0';
302
- case XPATH_RESULT_NODESET:
303
- return xpath_nodeset_count(result->value.nodeset_value) > 0;
309
+ case XPATH_RESULT_NODESET: {
310
+ XPathNodeSet* ns = result->value.nodeset_value;
311
+ size_t cnt = ns ? xpath_nodeset_count(ns) : 0;
312
+ /* XQuery sequence EBV: a singleton atomic carries the
313
+ * item's EBV (fn:remove round-trips a false boolean as
314
+ * falsy), while XPath node-existence EBV applies to
315
+ * real nodes and multi-item sequences. */
316
+ if (!ns || !ns->is_sequence) return cnt > 0;
317
+ if (cnt != 1) return cnt > 0;
318
+ void* nd = ns->nodes[0];
319
+ if (!nd || XPATH_NODE_TYPE(nd) != LEPTRIS_NODE_TEXT)
320
+ return 1;
321
+ const char* c = ((XPathTextNode*)nd)->content;
322
+ if (!c) return 1;
323
+ if (c[0] == '\x03') {
324
+ if (c[1] == 'B') return c[2] == 't';
325
+ if (c[1] == 'N' || c[1] == 'F' || c[1] == 'D') {
326
+ double d = strtod(c + 2, NULL);
327
+ return d != 0.0 && !isnan(d);
328
+ }
329
+ return 1;
330
+ }
331
+ return c[0] != '\0';
332
+ }
304
333
  default:
305
334
  return 0;
306
335
  }