leptris 1.9.237.1 → 1.9.239.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -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/vendor-src/README.md +1 -1
  16. data/vendor-src/libleptris/CHANGELOG.md +33 -0
  17. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  18. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +95 -11
  19. data/vendor-src/libleptris/src/leptris/leptris_internal.h +10 -0
  20. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +1 -0
  21. data/vendor-src/libleptris/src/leptris/xpath/evaluator_internal.h +1 -0
  22. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +39 -0
  23. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +32 -2
  24. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +15 -2
  25. data/vendor-src/libleptris/src/leptris/xpath/parser.c +70 -0
  26. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +7 -0
  27. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +5 -0
  28. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +2 -6
  29. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +81 -0
  30. data/vendor-src/libleptris/vcpkg.json +1 -1
  31. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4a6496fa19cc082a96e6edf44f894b398bc66fe4c8052f0588f4727696fdf59
4
- data.tar.gz: a4e5d44004b67e65bd14accd3546c5bbba3e31afe4c83f6b936b572c5f13bb41
3
+ metadata.gz: a9988358313da5babf4756d5a3393ff7a805eaabb0a6c04a240bf75173f8248b
4
+ data.tar.gz: 80ddb8c442105e7afad9a1cc40e010f670e115f61e295978157182889a6f2c6e
5
5
  SHA512:
6
- metadata.gz: b38d741b87da2cf151dfc6eacaa349aa8cb5db6705a868d7b0e37ca0f0c32e0960db5529ac12e473eec214f418131cf978513988ad85e8a2a8ddf0fe1da82521
7
- data.tar.gz: 6bff172e3608680967013be47594142fd1ca6ad82c12a7e546b8dbaf1e8898aa294dca06b8a90cf65cdef7714559371b160dc901eddc076fe21bf6d45d5efba4
6
+ metadata.gz: 194a8513e590353c118ff58cac0700789337ce254d8b138e5818a228134f75a5d40f2e4a54cbd8045900182bdb25ea8ca706efe5731e6ae0b3b086d16842f59a
7
+ data.tar.gz: 887dbf39367b2cf2397a493c59d2bb569212acefd4a11f8c1bcd5cbc167640d254e03f4099a71b11c3a9c5f19dcf7c1c75bc4831b6aee1b2533c3cf1f9c3a790
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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.239.0] - 2026-09-25
9
+
10
+ ### Fixed
11
+
12
+ - Vendored libleptris 1.9.239: XML 1.0 §2.11 end-of-line
13
+ normalization (#326) — parsed character data normalizes literal
14
+ CRLF and lone CR to LF before reference expansion (a CR character
15
+ reference stays CR), and attribute values collapse CRLF to ONE
16
+ space per §3.3.3. Nokogiri/libxml2 parity restored for any text
17
+ that ever carried \r\n. CDATA is not normalized. The
18
+ interleaved-lane hardening lands in the next engine sync.
19
+
8
20
  ## [1.9.237.0] - 2026-09-24
9
21
 
10
22
  ### Added
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.237"
11
+ LIBLEPTRIS_VERSION = "1.9.239"
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.237.1"
4
+ VERSION = "1.9.239.0"
5
5
  end
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.237
4
+ - libleptris v1.9.239
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.239] - 2026-09-24
4
+
5
+ ### Added
6
+
7
+ - xquery: shortest round-trip E notation for the XQuery number
8
+ spelling. Scientific values print the fewest mantissa digits that
9
+ read back as the same value — at float precision for xs:float
10
+ members (`3.4028235E38`, `2E6`, `4.9E-324`,
11
+ `1.7976931348623157E308`) — in canonical shape: E separator,
12
+ exponent sign only for negatives, no trailing zeros. The spelling
13
+ rides NUMBER results that escape an XQuery evaluation; the XPath
14
+ 1.0 surface keeps libxml2 xmlXPathFormatNumber parity.
15
+
16
+ ### Fixed
17
+
18
+ - QT3 corpus: fn:reverse 57→59, fn:distinct-values 87→89 adopted
19
+ (the dbl1args/flt1args E-notation pins).
20
+
21
+
22
+
23
+ ## [1.9.238] - 2026-09-24
24
+
25
+ ### Added
26
+
27
+ - xquery: computed `processing-instruction NAME { content }` and
28
+ `comment { content }` constructors. In the string-level
29
+ constructor model they serialize to their XML markup
30
+ (`<?NAME data?>` — empty content drops the data and the separating
31
+ space — and `<!--content-->`). The comment guard keys on the
32
+ brace, so the `comment()` node test stays intact.
33
+
34
+
35
+
3
36
  ## [1.9.237] - 2026-09-24
4
37
 
5
38
  <!-- Edit this section with the actual release notes. -->
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.237
6
+ VERSION 1.9.239
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -346,6 +346,8 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
346
346
  char* val, size_t val_len,
347
347
  int has_amp, int has_ws,
348
348
  const char* name_colon) {
349
+ /* Live value length — the §2.11 collapse (#326) shrinks it. */
350
+ size_t vlen = val_len;
349
351
  /* Attribute-value normalization (XML 1.0 §3.3.3, issue #576):
350
352
  * each literal tab/LF/CR in a CDATA attribute becomes a single
351
353
  * space. Character references (&#9;) are ASCII text here —
@@ -355,13 +357,21 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
355
357
  if (has_ws) {
356
358
  char* norm = (char*)leptris_pool_alloc(p->pool, val_len + 1);
357
359
  if (!norm) return -1;
360
+ /* §2.11 (#326): CRLF is ONE line break — it collapses to a
361
+ * single space (libxml2/pugixml parity) before the §3.3.3
362
+ * break-to-space mapping; lone CR likewise. */
363
+ size_t wl = 0;
358
364
  for (size_t i = 0; i < val_len; i++) {
359
365
  char ch = val[i];
360
- norm[i] = (ch == '\t' || ch == '\n' || ch == '\r')
361
- ? ' ' : ch;
366
+ if (ch == '\r' && i + 1 < val_len && val[i + 1] == '\n') {
367
+ i++;
368
+ }
369
+ norm[wl++] = (ch == '\t' || ch == '\n' || ch == '\r')
370
+ ? ' ' : ch;
362
371
  }
363
- norm[val_len] = '\0';
372
+ norm[wl] = '\0';
364
373
  val = norm;
374
+ vlen = wl;
365
375
  }
366
376
  struct leptris_attribute* attr = p->attr_cursor;
367
377
  if (DP_UNLIKELY(attr >= p->attr_end)) {
@@ -373,7 +383,7 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
373
383
  }
374
384
 
375
385
  attr->name_view = leptris_sv_from_ptr(name, name_len);
376
- leptris_attr_value_set_heap(attr, leptris_sv_from_ptr(val, val_len));
386
+ leptris_attr_value_set_heap(attr, leptris_sv_from_ptr(val, vlen));
377
387
  /* #1125: the name NUL (at name_end) and the value NUL (at the
378
388
  * closing quote) get restored after parse — both views must
379
389
  * materialize. The normalized/entity-expanded replacements below
@@ -387,9 +397,9 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
387
397
  * predefined entities lazily on first read.
388
398
  * - No '&': nothing to do. */
389
399
  unsigned ent = 0;
390
- if (val_len > 0 && has_amp) {
400
+ if (vlen > 0 && has_amp) {
391
401
  if (p->dtd) {
392
- LeptrisStringView dsv = leptris_sv_from_ptr(val, val_len);
402
+ LeptrisStringView dsv = leptris_sv_from_ptr(val, vlen);
393
403
  char* expanded = leptris_decode_entities_view_with_dtd(
394
404
  &dsv, p->dtd, p->pool);
395
405
  if (expanded) {
@@ -1018,6 +1028,46 @@ static inline LeptrisTextNode* dp_text_create(DParser* p,
1018
1028
  return tn;
1019
1029
  }
1020
1030
 
1031
+ /* §2.11 End-of-Line Handling (#326): collapse literal CR — CRLF →
1032
+ * LF, lone CR → LF — into a pool-owned NUL-terminated copy. Literal
1033
+ * bytes only: character references (&#xD;) are ASCII text at this
1034
+ * layer and must survive as CR (char refs sit outside §2.11
1035
+ * normalization). out_len receives the collapsed length. NULL on
1036
+ * allocation failure. */
1037
+ static char* dp_eol_collapse(LeptrisMemoryPool* pool, const char* s,
1038
+ size_t len, size_t* out_len) {
1039
+ char* conv = (char*)leptris_pool_alloc(pool, len + 1);
1040
+ if (!conv) return NULL;
1041
+ size_t wl = 0;
1042
+ for (size_t i = 0; i < len; i++) {
1043
+ char ch = s[i];
1044
+ if (ch == '\r') {
1045
+ conv[wl++] = '\n';
1046
+ if (i + 1 < len && s[i + 1] == '\n') i++;
1047
+ } else {
1048
+ conv[wl++] = ch;
1049
+ }
1050
+ }
1051
+ conv[wl] = '\0';
1052
+ *out_len = wl;
1053
+ return conv;
1054
+ }
1055
+
1056
+ /* §2.11 (#326) text mint for the keep-entity-refs segment paths:
1057
+ * zero-copy when the segment is CR-free, else a pool-owned
1058
+ * collapsed copy (leptris_text_create — ownership differs from the
1059
+ * borrowed carve; callers only stamp line/frozen + wire, both
1060
+ * ownership-agnostic). */
1061
+ static inline LeptrisTextNode* dp_text_create_norm(DParser* p,
1062
+ const char* s,
1063
+ size_t len) {
1064
+ if (memchr(s, '\r', len) == NULL) return dp_text_create(p, s, len);
1065
+ size_t wl;
1066
+ char* conv = dp_eol_collapse(p->pool, s, len, &wl);
1067
+ if (!conv) return NULL;
1068
+ return leptris_text_create(conv, wl, p->pool, p->doc);
1069
+ }
1070
+
1021
1071
  /* Internal: parse from a writable, NUL-terminated buffer.
1022
1072
  * owns_buffer: 1 = document frees buf on leptris_document_free,
1023
1073
  * 0 = caller owns buf (in-place mode). */
@@ -1329,7 +1379,7 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1329
1379
  (p.root_chained &&
1330
1380
  p.dc_tail == (LeptrisNode*)p.root))) {
1331
1381
  LeptrisTextNode* wtn =
1332
- dp_text_create(&p, ws_start, wl);
1382
+ dp_text_create_norm(&p, ws_start, wl);
1333
1383
  if (!wtn) goto fail;
1334
1384
  wtn->base.frozen = 1;
1335
1385
  dp_doc_child(&p, (LeptrisNode*)wtn);
@@ -1374,6 +1424,10 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1374
1424
  * entity-free docs. The memchr fallback covers the
1375
1425
  * >48-byte tail region only. */
1376
1426
  int has_amp = 0;
1427
+ /* §2.11 (#326): literal-CR tracking rides the same fused
1428
+ * loop — CR-bearing runs take the pool-copy collapse,
1429
+ * CR-free runs (the norm) keep the zero-copy carve. */
1430
+ int has_cr = 0;
1377
1431
  {
1378
1432
  const char* q = p.pos;
1379
1433
  const char* probe_end = p.probe_slack
@@ -1383,12 +1437,14 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1383
1437
  char ch = *q;
1384
1438
  if (ch == '<') { lt = (char*)q; goto text_done; }
1385
1439
  has_amp |= (ch == '&');
1440
+ has_cr |= (ch == '\r');
1386
1441
  q++;
1387
1442
  }
1388
1443
  if (q >= p.end) goto text_done;
1389
1444
  lt = (char*)memchr(q, '<', p.end - q);
1390
1445
  if (!lt) lt = p.end;
1391
1446
  has_amp |= memchr(q, '&', (size_t)((char*)lt - q)) != NULL;
1447
+ has_cr |= memchr(q, '\r', (size_t)((char*)lt - q)) != NULL;
1392
1448
  }
1393
1449
  text_done:
1394
1450
  p.pos = lt ? lt : p.end;
@@ -1447,7 +1503,7 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1447
1503
  }
1448
1504
  }
1449
1505
  if (!rt) {
1450
- rt = dp_text_create(&p, seg, slen);
1506
+ rt = dp_text_create_norm(&p, seg, slen);
1451
1507
  if (!rt) goto fail;
1452
1508
  /* #1285 slice 4: terminate the segment.
1453
1509
  * The '&' is dead: the walker has passed
@@ -1486,7 +1542,7 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1486
1542
  }
1487
1543
  }
1488
1544
  if (!rt) {
1489
- rt = dp_text_create(&p, seg, slen);
1545
+ rt = dp_text_create_norm(&p, seg, slen);
1490
1546
  if (!rt) goto fail;
1491
1547
  }
1492
1548
  rt->base.line = text_off;
@@ -1505,17 +1561,34 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1505
1561
  * raw terminated run. Entity-free runs pay one memchr
1506
1562
  * and stay zero-copy. */
1507
1563
  LeptrisTextNode* tn;
1564
+ /* §2.11 (#326): collapse literal CR BEFORE entity decode
1565
+ * — &#xD; expands after normalization and survives as
1566
+ * CR, exactly as the spec orders it. Collapsed runs are
1567
+ * pool-owned copies; CR-free runs keep every existing
1568
+ * path (entity-free stays zero-copy, entity-decode
1569
+ * fallback stays borrowed). */
1570
+ char* collapsed = NULL;
1571
+ size_t clen = tlen;
1572
+ if (has_cr) {
1573
+ collapsed = dp_eol_collapse(pool, text_start, tlen, &clen);
1574
+ if (!collapsed) goto fail;
1575
+ }
1508
1576
  if (has_amp) {
1509
- LeptrisStringView sv = leptris_sv_from_ptr(text_start, tlen);
1577
+ LeptrisStringView sv = leptris_sv_from_ptr(
1578
+ collapsed ? collapsed : text_start, clen);
1510
1579
  char* expanded = p.dtd
1511
1580
  ? leptris_decode_entities_view_with_dtd(
1512
1581
  &sv, p.dtd, pool)
1513
1582
  : leptris_decode_entities_view(&sv, pool);
1514
1583
  if (expanded) {
1515
1584
  tn = leptris_text_create(expanded, strlen(expanded), pool, p.doc);
1585
+ } else if (collapsed) {
1586
+ tn = leptris_text_create(collapsed, clen, pool, p.doc);
1516
1587
  } else {
1517
1588
  tn = dp_text_create(&p, text_start, tlen);
1518
1589
  }
1590
+ } else if (collapsed) {
1591
+ tn = leptris_text_create(collapsed, clen, pool, p.doc);
1519
1592
  } else {
1520
1593
  tn = dp_text_create(&p, text_start, tlen);
1521
1594
  }
@@ -1773,7 +1846,18 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1773
1846
  LeptrisCDATANode* cd = (LeptrisCDATANode*)
1774
1847
  dp_cpi_carve(&p, LEPTRIS_NODE_TYPE_CDATA);
1775
1848
  if (cd) {
1776
- cd->content = start; /* zero-copy, buf-lifetime */
1849
+ /* §2.11 (#326): CDATA content is Char data —
1850
+ * literal CR collapses to LF; CR-free content
1851
+ * keeps the zero-copy view. */
1852
+ if (memchr(start, '\r', (size_t)(p.pos - start))) {
1853
+ size_t wl;
1854
+ char* conv = dp_eol_collapse(
1855
+ pool, start, (size_t)(p.pos - start), &wl);
1856
+ if (!conv) goto fail;
1857
+ cd->content = conv;
1858
+ } else {
1859
+ cd->content = start; /* zero-copy, buf-lifetime */
1860
+ }
1777
1861
  } else {
1778
1862
  cd = leptris_cdata_create(start, p.pos - start, pool);
1779
1863
  if (!cd) goto fail;
@@ -654,6 +654,11 @@ struct leptris_xpath_result {
654
654
  * NULL for the untyped/string class. NOT owned — static
655
655
  * literals only. Cleared in result_new (free-list recycle). */
656
656
  const char* atomic_type;
657
+ /* XQuery number spelling: set on NUMBER results that escape an
658
+ * XQuery evaluation, so result_string renders the shortest
659
+ * round-trip E form instead of the libxml2-parity XPath
660
+ * spelling. Cleared in result_new (free-list recycle). */
661
+ int xq_spelling;
657
662
  };
658
663
 
659
664
  /* Namespace mapping for XPath context (v0.8.0) */
@@ -825,6 +830,11 @@ typedef enum {
825
830
  * joined by '\x01' ("*" catches all), children[0] = try body,
826
831
  * children[1..] = catch bodies in order. */
827
832
  XPATH_OP_DOCUMENT_CTOR,
833
+ /* processing-instruction NAME { content } and
834
+ * comment { content } — the string-level ctor model serializes
835
+ * to the XML markup form (QT3 cbcl-deep-equal-002..004). */
836
+ XPATH_OP_PI_CTOR,
837
+ XPATH_OP_COMMENT_CTOR,
828
838
  XPATH_OP_TRY,
829
839
  /* XQuery 3.0 typeswitch: value = case SequenceTypes joined
830
840
  * by '\x01' (a trailing empty entry = the default arm),
@@ -743,6 +743,7 @@ struct leptris_xpath_result* xpath_result_new(XPathResultType type) {
743
743
  result->type = type;
744
744
  result->is_int = 0; /* free-list recycling would leak it */
745
745
  result->atomic_type = NULL;
746
+ result->xq_spelling = 0;
746
747
 
747
748
  /* Initialize union based on type. XPATH_RESULT_CACHED is the
748
749
  * free-list sentinel — a fresh result never carries it. */
@@ -21,6 +21,7 @@ char* xpath_to_string(struct leptris_xpath_result* result);
21
21
  char* get_node_text(void* node);
22
22
  char* xpath_number_to_string(double number);
23
23
  char* xpath_number_to_string_xq(double number);
24
+ char* xpath_number_to_string_xq_typed(double number, int float_prec);
24
25
  char* xpath_int_to_string(long long v);
25
26
 
26
27
  /* From evaluator_operators.c — synthetic sequence members and the
@@ -1521,6 +1521,45 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1521
1521
  * escape &<"', text content escapes &<; raw expression content
1522
1522
  * passes through (a nested constructor's result is already
1523
1523
  * markup; arbitrary-string escaping is a value-model limit). */
1524
+ /* Computed PI/comment constructors (string-level ctor model):
1525
+ * serialize to the XML markup form. PI data is raw (no
1526
+ * entity-escaping in the data position); empty content drops
1527
+ * the data and the separating space. */
1528
+ if (op == XPATH_OP_PI_CTOR || op == XPATH_OP_COMMENT_CTOR) {
1529
+ char* s = NULL;
1530
+ if (ast->child_count >= 1) {
1531
+ struct leptris_xpath_result* v =
1532
+ evaluate_expr(ctx, ast->children[0]);
1533
+ if (v) s = xpath_to_string(v);
1534
+ xpath_result_free(v);
1535
+ }
1536
+ if (!s) s = leptris_strdup("");
1537
+ struct leptris_xpath_result* out =
1538
+ xpath_result_new(XPATH_RESULT_STRING);
1539
+ if (!out) {
1540
+ free(s);
1541
+ return NULL;
1542
+ }
1543
+ const char* target =
1544
+ op == XPATH_OP_PI_CTOR && ast->value ? ast->value : "";
1545
+ size_t tl = strlen(target);
1546
+ size_t dl = strlen(s);
1547
+ char* buf = (char*)malloc(tl + dl + 8);
1548
+ if (!buf) {
1549
+ free(s);
1550
+ xpath_result_free(out);
1551
+ return NULL;
1552
+ }
1553
+ if (op == XPATH_OP_PI_CTOR)
1554
+ snprintf(buf, tl + dl + 8, "<?%s%s%s?>", target,
1555
+ dl ? " " : "", s);
1556
+ else
1557
+ snprintf(buf, dl + 8, "<!--%s-->", s);
1558
+ out->value.string_value = buf;
1559
+ free(s);
1560
+ return out;
1561
+ }
1562
+
1524
1563
  if (op == XPATH_OP_TEXT_CTOR || op == XPATH_OP_ATTRIBUTE_CTOR ||
1525
1564
  op == XPATH_OP_ELEMENT_CTOR) {
1526
1565
  if (op != XPATH_OP_ELEMENT_CTOR) {
@@ -88,6 +88,13 @@ char* xpath_number_to_string(double number) {
88
88
  * XsltNumberFormat.MatchesLibxml2 and the libxslt suite), so this
89
89
  * spelling is only used where the context sets xquery_spelling. */
90
90
  char* xpath_number_to_string_xq(double number) {
91
+ return xpath_number_to_string_xq_typed(number, 0);
92
+ }
93
+
94
+ /* float_prec: the value is a float32-widened double — the shortest
95
+ * round-trip loop reads back at float precision so xs:float spells
96
+ * "3.4028235E38", not the widened-double digits. */
97
+ char* xpath_number_to_string_xq_typed(double number, int float_prec) {
91
98
  if (isnan(number)) return leptris_strdup("NaN");
92
99
  if (isinf(number))
93
100
  return leptris_strdup(number > 0 ? "INF" : "-INF");
@@ -103,8 +110,31 @@ char* xpath_number_to_string_xq(double number) {
103
110
  int size;
104
111
  double absolute_value = fabs(number);
105
112
  if (absolute_value > 1e18 || absolute_value < 1e-18) {
106
- size = (int)snprintf(work, sizeof(work), "%21.14e", number);
107
- while (size > 0 && work[size] != 'e') size--;
113
+ /* Shortest round-trip scientific spelling (Saxon/QT3): the
114
+ * fewest mantissa digits that read back as the same double,
115
+ * in canonical form — E separator, exponent sign only for
116
+ * negatives, no trailing zeros ("1.7976931348623157E308",
117
+ * "2E6", "4.9E-324"). Returns directly: the shared trim
118
+ * below must not eat exponent digits. */
119
+ char mant[64];
120
+ int prec;
121
+ for (prec = 1; prec <= 17; prec++) {
122
+ snprintf(mant, sizeof(mant), "%.*e", prec, number);
123
+ double back = strtod(mant, NULL);
124
+ if (float_prec) back = (double)((float)back);
125
+ if (back == number) break;
126
+ }
127
+ char* e = strchr(mant, 'e');
128
+ if (!e) return leptris_strdup(mant);
129
+ *e = 0;
130
+ char* last = mant + strlen(mant);
131
+ while (last > mant && *(last - 1) == '0') last--;
132
+ if (last > mant && *(last - 1) == '.') last--;
133
+ *last = 0;
134
+ const char* exp = e + 1;
135
+ if (*exp == '+') exp++;
136
+ snprintf(work, sizeof(work), "%sE%s", mant, exp);
137
+ return leptris_strdup(work);
108
138
  } else {
109
139
  /* Fraction budget: integer digits first, the fraction spends
110
140
  * the remaining significant digits (33 places floor for the
@@ -160,7 +160,15 @@ static char** collect_items_raw(XPathContext* ctx, XPathASTNode** args,
160
160
  }
161
161
  }
162
162
  } else if (r->type == XPATH_RESULT_NUMBER) {
163
- char* s = scalar_str(r);
163
+ /* XQuery-spelled doubles (shortest round-trip E form) when
164
+ * the eval is XQuery-flavored — same rule as the sequence
165
+ * operator's scalar spread. */
166
+ char* s = (ctx->xquery_spelling && !r->is_int)
167
+ ? xpath_number_to_string_xq_typed(
168
+ r->value.number_value,
169
+ r->atomic_type &&
170
+ strcmp(r->atomic_type, "xs:float") == 0)
171
+ : scalar_str(r);
164
172
  size_t sl = s ? strlen(s) : 0;
165
173
  items = (char**)malloc(sizeof(char*));
166
174
  if (!items) { free(s); xpath_result_free(r); return NULL; }
@@ -441,7 +449,12 @@ static struct leptris_xpath_result* fn_index_of(XPathContext* ctx,
441
449
  needle = leptris_strdup("");
442
450
  }
443
451
  } else if (v && v->type == XPATH_RESULT_NUMBER) {
444
- char* s = scalar_str(v);
452
+ char* s = (ctx->xquery_spelling && !v->is_int)
453
+ ? xpath_number_to_string_xq_typed(
454
+ v->value.number_value,
455
+ v->atomic_type &&
456
+ strcmp(v->atomic_type, "xs:float") == 0)
457
+ : scalar_str(v);
445
458
  size_t sl = s ? strlen(s) : 0;
446
459
  needle = (char*)malloc(sl + 3);
447
460
  if (needle) {
@@ -1453,6 +1453,76 @@ static XPathASTNode* parse_path_expr(XPathParser* parser) {
1453
1453
  tc->value = leptris_strdup(tests);
1454
1454
  return parse_postfix_ops(parser, tc);
1455
1455
  }
1456
+ /* processing-instruction NAME { content } — the computed
1457
+ * PI constructor. The target is the NCNAME/QNAME; the
1458
+ * optional content serializes as the PI data. */
1459
+ else if (current_token(parser)->type == TOK_NCNAME &&
1460
+ current_token(parser)->value_len == 22 &&
1461
+ memcmp(current_token(parser)->value,
1462
+ "processing-instruction", 22) == 0 &&
1463
+ next && (next->type == TOK_NCNAME ||
1464
+ next->type == TOK_QNAME) &&
1465
+ parser->token_pos + 2 < parser->token_count &&
1466
+ parser->tokens[parser->token_pos + 2].type ==
1467
+ TOK_LBRACE) {
1468
+ char name[128];
1469
+ size_t nl = next->value_len;
1470
+ if (nl >= sizeof(name)) return NULL;
1471
+ memcpy(name, next->value, nl);
1472
+ name[nl] = 0;
1473
+ advance_token(parser);
1474
+ advance_token(parser);
1475
+ advance_token(parser);
1476
+ XPathASTNode* pc = ast_node_new(XPATH_AST_OPERATOR);
1477
+ if (!pc) return NULL;
1478
+ pc->number_value = (double)XPATH_OP_PI_CTOR;
1479
+ pc->value = leptris_strdup(name);
1480
+ if (current_token_is(parser, TOK_RBRACE)) {
1481
+ XPathASTNode* empty = ast_node_new(XPATH_AST_OPERATOR);
1482
+ if (!empty) { ast_node_free(pc); return NULL; }
1483
+ empty->number_value = (double)XPATH_OP_SEQUENCE;
1484
+ ast_node_add_child(pc, empty);
1485
+ } else {
1486
+ XPathASTNode* item = parse_expr(parser);
1487
+ if (!item) { ast_node_free(pc); return NULL; }
1488
+ ast_node_add_child(pc, item);
1489
+ }
1490
+ if (!current_token_is(parser, TOK_RBRACE)) {
1491
+ ast_node_free(pc);
1492
+ return NULL;
1493
+ }
1494
+ advance_token(parser);
1495
+ return parse_postfix_ops(parser, pc);
1496
+ }
1497
+ /* comment { content } — the computed comment constructor.
1498
+ * Guarded by the brace so the comment() node test stays
1499
+ * intact. */
1500
+ else if (current_token(parser)->type == TOK_NCNAME &&
1501
+ next && next->type == TOK_LBRACE &&
1502
+ current_token(parser)->value_len == 7 &&
1503
+ memcmp(current_token(parser)->value, "comment", 7) == 0) {
1504
+ advance_token(parser);
1505
+ advance_token(parser);
1506
+ XPathASTNode* cc = ast_node_new(XPATH_AST_OPERATOR);
1507
+ if (!cc) return NULL;
1508
+ cc->number_value = (double)XPATH_OP_COMMENT_CTOR;
1509
+ if (current_token_is(parser, TOK_RBRACE)) {
1510
+ XPathASTNode* empty = ast_node_new(XPATH_AST_OPERATOR);
1511
+ if (!empty) { ast_node_free(cc); return NULL; }
1512
+ empty->number_value = (double)XPATH_OP_SEQUENCE;
1513
+ ast_node_add_child(cc, empty);
1514
+ } else {
1515
+ XPathASTNode* item = parse_expr(parser);
1516
+ if (!item) { ast_node_free(cc); return NULL; }
1517
+ ast_node_add_child(cc, item);
1518
+ }
1519
+ if (!current_token_is(parser, TOK_RBRACE)) {
1520
+ ast_node_free(cc);
1521
+ return NULL;
1522
+ }
1523
+ advance_token(parser);
1524
+ return parse_postfix_ops(parser, cc);
1525
+ }
1456
1526
  else if (current_token(parser)->type == TOK_NCNAME &&
1457
1527
  next && next->type == TOK_LBRACE &&
1458
1528
  current_token(parser)->value_len == 4 &&
@@ -486,6 +486,13 @@ LEPTRIS_API char* leptris_xpath_result_string(LeptrisXPathResult result) {
486
486
  case XPATH_RESULT_BOOLEAN:
487
487
  return leptris_strdup(result->value.boolean_value ? "true" : "false");
488
488
  case XPATH_RESULT_NUMBER:
489
+ if (result->xq_spelling) {
490
+ char* s = xpath_number_to_string_xq_typed(
491
+ result->value.number_value,
492
+ result->atomic_type &&
493
+ strcmp(result->atomic_type, "xs:float") == 0);
494
+ if (s) return s;
495
+ }
489
496
  if (result->is_int) return xpath_int_to_string(result->int_value);
490
497
  return xpath_number_to_string(result->value.number_value);
491
498
  case XPATH_RESULT_NODESET:
@@ -3168,6 +3168,11 @@ static LeptrisXPathResult xq_eval_impl(
3168
3168
  else
3169
3169
  free(s);
3170
3170
  }
3171
+ /* XQuery number spelling rides the result: NUMBER returns render
3172
+ * via the shortest round-trip E form in result_string (the
3173
+ * libxml2-parity XPath printer stays for the XPath surface). */
3174
+ if (result && result->type == XPATH_RESULT_NUMBER && !result->is_int)
3175
+ result->xq_spelling = 1;
3171
3176
  return result;
3172
3177
  }
3173
3178
 
@@ -922,12 +922,10 @@ TEST(Qt3Subset, FnRemove) {
922
922
 
923
923
  TEST(Qt3Subset, FnReverse) {
924
924
  /* Reds are the typed-atom class (NaN/INF/boolean member compare). */
925
- run_test_set("fn/reverse.xml", {}, 57, {},
925
+ run_test_set("fn/reverse.xml", {}, 59, {},
926
926
  {
927
927
  "K2-SeqReverseFunc-1",
928
928
  "K2-SeqReverseFunc-2",
929
- "fn-reversedbl1args-1",
930
- "fn-reversedbl1args-3",
931
929
 
932
930
  });
933
931
  }
@@ -961,7 +959,7 @@ TEST(Qt3Subset, FnDistinctValues) {
961
959
  /* mixed-args-012 (decimal-vs-float dedup) diverges ONLY inside
962
960
  * the LTO'd test binary — identical query passes standalone and
963
961
  * via the public API; CI's non-LTO/gcc legs are the arbiter. */
964
- run_test_set("fn/distinct-values.xml", {}, 87, {},
962
+ run_test_set("fn/distinct-values.xml", {}, 89, {},
965
963
  {
966
964
  "cbcl-distinct-values-002",
967
965
  "cbcl-distinct-values-002b",
@@ -975,8 +973,6 @@ TEST(Qt3Subset, FnDistinctValues) {
975
973
  "fn-distinct-values-mixed-args-010",
976
974
  "fn-distinct-values-mixed-args-012",
977
975
  "fn-distinct-values-mixed-args-018",
978
- "fn-distinct-valuesdbl1args-1",
979
- "fn-distinct-valuesdbl1args-3",
980
976
 
981
977
  });
982
978
  }
@@ -821,3 +821,84 @@ TEST(XQueryLeaks, RepeatedFunctionDeclarationEvalsDoNotLeak) {
821
821
  }
822
822
  leptris_document_free(doc);
823
823
  }
824
+
825
+ /* Computed PI and comment constructors (QT3 cbcl-deep-equal-002..004
826
+ * class): the string-level ctor model serializes them to their XML
827
+ * markup, so deep-equal compares target+data / comment text. */
828
+ TEST(XQueryCtors, PIConstructor) {
829
+ LeptrisStatus st = LEPTRIS_OK;
830
+ LeptrisDocument doc = leptris_parse_string("<e/>", 4, &st);
831
+ ASSERT_NE(doc, nullptr);
832
+ struct {
833
+ const char* q;
834
+ const char* want;
835
+ } cases[] = {
836
+ {"processing-instruction target { \"data\" }", "<?target data?>"},
837
+ {"processing-instruction target { \"\" }", "<?target?>"},
838
+ {"processing-instruction target { () }", "<?target?>"},
839
+ };
840
+ for (auto& c : cases) {
841
+ LeptrisXQuery xq = leptris_xquery_parse(c.q, strlen(c.q));
842
+ ASSERT_NE(xq, nullptr) << c.q;
843
+ LeptrisXPathResult r = leptris_xquery_eval(xq, doc, NULL);
844
+ ASSERT_NE(r, nullptr) << c.q;
845
+ char* s = leptris_xpath_result_string(r);
846
+ ASSERT_NE(s, nullptr) << c.q;
847
+ EXPECT_STREQ(s, c.want) << c.q;
848
+ leptris_free_string(s);
849
+ leptris_xpath_result_free(r);
850
+ leptris_xquery_free(xq);
851
+ }
852
+ leptris_document_free(doc);
853
+ }
854
+
855
+ TEST(XQueryCtors, CommentConstructor) {
856
+ LeptrisStatus st = LEPTRIS_OK;
857
+ LeptrisDocument doc = leptris_parse_string("<e/>", 4, &st);
858
+ ASSERT_NE(doc, nullptr);
859
+ const char* cq = "comment { \"hello\" }";
860
+ LeptrisXQuery xq = leptris_xquery_parse(cq, strlen(cq));
861
+ ASSERT_NE(xq, nullptr);
862
+ LeptrisXPathResult r = leptris_xquery_eval(xq, doc, NULL);
863
+ ASSERT_NE(r, nullptr);
864
+ char* s = leptris_xpath_result_string(r);
865
+ ASSERT_NE(s, nullptr);
866
+ EXPECT_STREQ(s, "<!--hello-->");
867
+ leptris_free_string(s);
868
+ leptris_xpath_result_free(r);
869
+ leptris_xquery_free(xq);
870
+ leptris_document_free(doc);
871
+ }
872
+
873
+ TEST(XQueryCtors, PICtorDeepEqualByTargetAndData) {
874
+ LeptrisStatus st = LEPTRIS_OK;
875
+ LeptrisDocument doc = leptris_parse_string("<e/>", 4, &st);
876
+ ASSERT_NE(doc, nullptr);
877
+ struct {
878
+ const char* q;
879
+ bool want;
880
+ } cases[] = {
881
+ {"deep-equal(processing-instruction cheese { \"brie\" }, "
882
+ "processing-instruction cheese { \"brie\" })",
883
+ true},
884
+ {"deep-equal(processing-instruction cheese { \"brie\" }, "
885
+ "processing-instruction cheese { \"stilton\" })",
886
+ false},
887
+ {"deep-equal(processing-instruction foo { \"bar\" }, "
888
+ "processing-instruction bar { \"foo\" })",
889
+ false},
890
+ };
891
+ for (auto& c : cases) {
892
+ LeptrisXQuery xq = leptris_xquery_parse(c.q, strlen(c.q));
893
+ ASSERT_NE(xq, nullptr) << c.q;
894
+ LeptrisXPathResult r = leptris_xquery_eval(xq, doc, NULL);
895
+ ASSERT_NE(r, nullptr) << c.q;
896
+ char* s = leptris_xpath_result_string(r);
897
+ ASSERT_NE(s, nullptr) << c.q;
898
+ EXPECT_STREQ(s, c.want ? "true" : "false") << c.q;
899
+ leptris_free_string(s);
900
+ leptris_xpath_result_free(r);
901
+ leptris_xquery_free(xq);
902
+ }
903
+ leptris_document_free(doc);
904
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.237",
4
+ "version": "1.9.239",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.237.1
4
+ version: 1.9.239.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-24 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi