leptris 1.9.285.1-ppc64le-linux → 1.9.290.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +59 -4
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/descriptor.rb +12 -0
  7. data/lib/leptris/xml/element.rb +36 -1
  8. data/lib/leptris/xml/ffi.rb +15 -2
  9. data/lib/leptris/xml/native.so +0 -0
  10. data/lib/leptris/xml/xslt.rb +52 -6
  11. data/lib/libleptris.so +0 -0
  12. data/vendor-src/README.md +1 -1
  13. data/vendor-src/libleptris/CHANGELOG.md +74 -0
  14. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  15. data/vendor-src/libleptris/src/include/leptris/descriptor.h +12 -0
  16. data/vendor-src/libleptris/src/include/leptris.h +44 -1
  17. data/vendor-src/libleptris/src/leptris/common/chartype.c +4 -0
  18. data/vendor-src/libleptris/src/leptris/common/chartype.h +10 -0
  19. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +72 -1
  20. data/vendor-src/libleptris/src/leptris/dom/element.c +6 -4
  21. data/vendor-src/libleptris/src/leptris/dom/element.h +79 -36
  22. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +18 -14
  23. data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +3 -3
  24. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +49 -21
  25. data/vendor-src/libleptris/src/leptris/html/html_parse.c +191 -152
  26. data/vendor-src/libleptris/src/leptris/sax/recorder.c +3 -0
  27. data/vendor-src/libleptris/src/leptris/sax/records.c +8 -1
  28. data/vendor-src/libleptris/src/leptris/serialize/serialize.c +11 -8
  29. data/vendor-src/libleptris/src/leptris/xpath/vm.c +5 -3
  30. data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +76 -8
  31. data/vendor-src/libleptris/src/leptris/xslt/xslt_internal.h +16 -1
  32. data/vendor-src/libleptris/src/leptris/xslt/xslt_public.c +38 -8
  33. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +74 -0
  34. data/vendor-src/libleptris/test/abi/test_header_hygiene.cpp +35 -10
  35. data/vendor-src/libleptris/test/sax/test_recorder.cpp +24 -0
  36. data/vendor-src/libleptris/test/sax/test_records.cpp +25 -0
  37. data/vendor-src/libleptris/test/xpath/test_xslt.cpp +151 -0
  38. data/vendor-src/libleptris/test/xquery/qt3/fn/not.xml +771 -0
  39. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +11 -0
  40. data/vendor-src/libleptris/vcpkg.json +1 -1
  41. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e305ee50f7fa5119eb8cf30665f5710954d9165d34bfdc2553456600454246f0
4
- data.tar.gz: ce035151452f84bd5bd185765a19daf0ac7318b32bc0db1ce3e3c3b7505547f4
3
+ metadata.gz: 505fb9e6093b335b955a6c776e97beb0cd43d9e498d8f44333b3faccc93bee26
4
+ data.tar.gz: 69b07068ad720f0da28fbc713625884621cf067a0fe0134b88bed2eba8549fa5
5
5
  SHA512:
6
- metadata.gz: 87fb81993e4eaf0f5efbcc52ebf2edcbe01e2c14ae48949e7b6589b7066880e78511ce10ff2e4cbb497016bf3cd68350a826ad26500f1d6fcb2226172ec3985f
7
- data.tar.gz: aa5cdd0872af74dfd273b2b85fec1ad2613cf80c14301d3b0a0c275078b1333378b2f8d6e58aed768856cad0119866f50fe871ac0840118fa55bffc75964f28b
6
+ metadata.gz: aba2088b46d2f97563715259e4a8c8f12fa49f5918a42272f849c64030aad24fa0051449e9f29a0e90c5f7e118be49cc8dc9eae14f4575f4c7552c524715205c
7
+ data.tar.gz: ea9d90fb9c71b40bcd3692b81d138752a9e4d5aca9943baa6f6a62769f1136773cb0b54e28ed36e0c7ca4a46c7e40b874351f4d76ebdbf227d64611f38502bad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## [1.9.287.0] - 2026-10-02
2
+
3
+ ### Added
4
+
5
+ - **Stylesheet top-level params (#360)**: `Stylesheet#apply_to(doc,
6
+ params: {name => value})` and `#serialize(doc, params: {...})`
7
+ thread XSLT `<xsl:param>` overrides through the new engine faces
8
+ (libleptris 1.9.287, leptris#1478): a supplied name binds its
9
+ string directly and never evaluates select/@default; absent names
10
+ keep them. Values bind as XPath strings — the caller owns
11
+ numeric/boolean quoting (nokogiri quote_params conventions for
12
+ moxml parity). nil/empty params keeps the flagless v1 faces.
13
+
14
+ ### Fixed
15
+
16
+ - **NativeNode#content on comment nodes (#344)**: the C-side walk
17
+ accessor now dispatches by node kind — comment and CDATA payloads
18
+ read through their dedicated engine accessors (comment-kind nodes
19
+ answered nil while text/CDATA worked); element aggregation and
20
+ text reads unchanged.
21
+
22
+ ### Engine sync (1.9.287)
23
+
24
+ - leptris_xslt_apply_params / leptris_xslt_apply_string_params
25
+ (leptris#1478) — the param overrides ride above.
26
+
1
27
  ## [1.9.285.0] - 2026-10-01
2
28
 
3
29
  ### Added — streaming Door A opt-outs (libleptris 1.9.283, #1459)
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.285"
11
+ LIBLEPTRIS_VERSION = "1.9.290"
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
@@ -17,7 +17,6 @@
17
17
 
18
18
  typedef const char *(*elem_name_fn)(void *);
19
19
  typedef const char *(*text_content_fn)(void *);
20
-
21
20
  /* Engine node-kind ids (leptris_node_get_type) — hoisted above the
22
21
  * read accessors so every dispatch shares one spelling. */
23
22
  #define WS_NODE_ELEMENT 0
@@ -330,9 +329,9 @@ static VALUE nn_content(VALUE self)
330
329
  if (doc != Qnil && n->content_ver != Qnil &&
331
330
  n->content_ver == rb_ivar_get(doc, id_iv_version))
332
331
  return n->content_val;
333
- /* #344: every kind reads through its own accessor — the old
334
- * element-vs-text ternary handed comments and PIs to the text
335
- * accessor, which answers NULL for them. */
332
+ /* every kind through its own accessor (#344): elements
333
+ * aggregate, text/comment/CDATA/PI carry their payload
334
+ * directly; unknown kinds answer nil. */
336
335
  switch (f_node_type(n->ptr)) {
337
336
  case WS_NODE_ELEMENT: s = f_element_text(n->ptr); break;
338
337
  case WS_NODE_TEXT: s = f_text_content(n->ptr); break;
@@ -795,6 +794,54 @@ static VALUE nf_append_binding_child(VALUE self, VALUE document,
795
794
  return Qtrue;
796
795
  }
797
796
 
797
+ /* Batch append (leptris-ruby#366): one crossing for N children.
798
+ * Same gates and provable no-op lift predicate as
799
+ * nf_append_binding_child; ONE version bump covers the batch (a
800
+ * failed mutation discards memos either way). Children are passed
801
+ * as pre-flattened element ADDRESSES (Integer) — the Ruby side
802
+ * keeps the node objects for the cross-document invalidation
803
+ * sweep, which is pure Ruby and needs no crossing.
804
+ *
805
+ * Returns the index of the first child that needs the Ruby-side
806
+ * namespace-lift path (source order preserved: the caller runs
807
+ * the full per-child path from there); n when every child was
808
+ * appended. Status failures raise at the failing child — earlier
809
+ * appends stay appended, exactly like the per-child face. */
810
+ static VALUE nf_append_binding_children(VALUE self, VALUE document,
811
+ VALUE parent_addr, VALUE addrs)
812
+ {
813
+ void *parent;
814
+ const char *uri;
815
+ long i, n;
816
+ int st;
817
+
818
+ (void)self;
819
+ resolve_binding_classes();
820
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
821
+ rb_raise(c_use_after_free_error,
822
+ "owning document has been freed");
823
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
824
+ rb_raise(c_readonly_error,
825
+ "document is readonly — mutation attempted");
826
+ Check_Type(addrs, T_ARRAY);
827
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
828
+ n = RARRAY_LEN(addrs);
829
+ rb_ivar_set(document, id_iv_version,
830
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
831
+ for (i = 0; i < n; i++) {
832
+ void *child = (void *)(uintptr_t)NUM2ULL(RARRAY_AREF(addrs, i));
833
+ if (f_node_type(child) == NT_ELEMENT) {
834
+ if (f_elem_ns_count(child) > 0)
835
+ return LONG2NUM(i);
836
+ uri = f_elem_ns(child);
837
+ if (uri && *uri)
838
+ return LONG2NUM(i);
839
+ }
840
+ st = f_append_child(parent, child);
841
+ check_status_c(st);
842
+ }
843
+ return LONG2NUM(n);
844
+ }
798
845
  /* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
799
846
  * gates, the version bump (which drops the version-stamped
800
847
  * attribute memos on both surfaces), and the engine write in one
@@ -1984,6 +2031,12 @@ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
1984
2031
  * construction — the consumer iterates rows and builds their
1985
2032
  * typed objects directly. */
1986
2033
  /* Node kinds (mirror types.h). Documented in descriptor comment. */
2034
+ #define WS_NODE_ELEMENT 0
2035
+ #define WS_NODE_TEXT 1
2036
+ #define WS_NODE_COMMENT 2
2037
+ #define WS_NODE_CDATA 3
2038
+ #define WS_NODE_PI 4
2039
+ #define WS_NODE_DOCTYPE 5
1987
2040
 
1988
2041
  static VALUE build_element_row(VALUE doc, void *elem, int depth)
1989
2042
  {
@@ -3030,6 +3083,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
3030
3083
  rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
3031
3084
  rb_define_module_function(m_native, "fast_attribute2", nf_fast_attribute2, 2);
3032
3085
  rb_define_module_function(m_native, "ns_lift_needed?", nf_ns_lift_needed, 1);
3086
+ rb_define_module_function(m_native, "append_binding_children",
3087
+ nf_append_binding_children, 3);
3033
3088
  rb_define_module_function(m_native, "append_binding_child",
3034
3089
  nf_append_binding_child, 3);
3035
3090
  rb_define_module_function(m_native, "set_binding_attribute",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.285.1"
4
+ VERSION = "1.9.290.0"
5
5
  end
@@ -167,6 +167,18 @@ class Leptris::XML::Descriptor
167
167
  ap[:wire_name] = anchor_string(anchors, row.fetch(:name))
168
168
  ap[:kind] = kind_code(row[:kind] || :scalar)
169
169
  ap[:type_tag] = type_tag_code(row)
170
+ # Attribute-level namespace form (libleptris 1.9.289, #1486):
171
+ # ns rows match by (URI, local) — the wire prefix is irrelevant.
172
+ # Mirrors the child-row packing.
173
+ ap[:ns_form] =
174
+ case row[:ns]
175
+ when :any then Leptris::XML::FFI::PLAN_NS_ANY
176
+ when Hash then Leptris::XML::FFI::PLAN_NS_EXACT
177
+ else Leptris::XML::FFI::PLAN_NS_NONE
178
+ end
179
+ ap[:ns_uri] =
180
+ row[:ns].is_a?(Hash) ?
181
+ anchor_string(anchors, row[:ns].fetch(:exact)) : nil
170
182
  pack_predicates(ap, row, anchors)
171
183
  end
172
184
  ep[:attribute_count] = attrs.size
@@ -490,7 +490,42 @@ class Leptris::XML::Element < Leptris::XML::Node
490
490
  # Remove existing children, then attach the new ones in source order.
491
491
  Leptris::XML::FFI.check_status(
492
492
  Leptris::XML::FFI.leptris_element_remove_children(c_ptr))
493
- Array(node_or_nodes).each { |n| add_child(n) }
493
+ append_children(Array(node_or_nodes))
494
+ end
495
+
496
+ # Batch append (leptris-ruby#366): one C crossing for N children.
497
+ # The C loop runs the same gates and provable no-op lift
498
+ # predicate as the per-child fast path with ONE version bump for
499
+ # the batch; it stops at the first child needing the Ruby-side
500
+ # namespace lift (source order preserved — the remainder goes
501
+ # through the full per-child path). Strings and non-Nodes fall
502
+ # back to add_child. The cross-document memo invalidation sweep
503
+ # is pure Ruby and stays on this side.
504
+ def append_children(nodes)
505
+ ensure_writable!
506
+ return if nodes.empty?
507
+ unless nodes.all? { |n| n.is_a?(Leptris::XML::Node) }
508
+ nodes.each { |n| add_child(n) }
509
+ return
510
+ end
511
+ if native_fast_children?
512
+ addrs = nodes.map { |n| n.c_ptr.address }
513
+ done = Leptris::XML::Native.append_binding_children(
514
+ @document, @c_address, addrs)
515
+ done = 0 unless done.is_a?(Integer)
516
+ if done >= nodes.size
517
+ nodes.each do |n|
518
+ Leptris::XML::Node.invalidate_cross_document!(n, @document)
519
+ end
520
+ return nodes
521
+ end
522
+ nodes[0, done].each do |n|
523
+ Leptris::XML::Node.invalidate_cross_document!(n, @document)
524
+ end
525
+ nodes = nodes[done..]
526
+ end
527
+ nodes.each { |n| add_child(n) }
528
+ nodes
494
529
  end
495
530
 
496
531
  # Replace this element with +new_node+ in the parent's child list.
@@ -299,7 +299,13 @@ module Leptris
299
299
  :type_tag, :uint8,
300
300
  :predicate_count, :uint16,
301
301
  :pad_pred, :uint16,
302
- :predicates, :pointer
302
+ :predicates, :pointer,
303
+ # libleptris 1.9.289 (#1486): attribute-level namespace
304
+ # form — ns rows match by (URI, local), wire_name is the
305
+ # LOCAL name; NONE keeps wire-name-exact matching.
306
+ :ns_form, :uint8,
307
+ :pad_ns, :uint8,
308
+ :ns_uri, :pointer
303
309
  end
304
310
  class ChildPlan < ::FFI::Struct
305
311
  layout :wire_name, :pointer,
@@ -1272,7 +1278,14 @@ attach_function :leptris_parse_string,
1272
1278
  # apply + serialize in one call, keeping top-level text nodes
1273
1279
  # and result fragments the tree API would flatten.
1274
1280
  attach_function :leptris_xslt_apply_string,
1275
- [:leptris_xslt, :leptris_document], :pointer
1281
+ [:leptris_xslt, :leptris_document], :pointer # Top-level xsl:param overrides (#360): flat name/value pairs,
1282
+ # 2*pair_count entries; supplied names skip select/@default.
1283
+ attach_function :leptris_xslt_apply_params,
1284
+ [:leptris_xslt, :leptris_document, :pointer, :size_t],
1285
+ :leptris_document
1286
+ attach_function :leptris_xslt_apply_string_params,
1287
+ [:leptris_xslt, :leptris_document, :pointer, :size_t],
1288
+ :pointer
1276
1289
  attach_function :leptris_c14n_canonicalize,
1277
1290
 
1278
1291
  [:leptris_document, :int, :int], :pointer
Binary file
@@ -28,6 +28,9 @@ module Leptris::XML::XSLT
28
28
  # # text nodes preserved)
29
29
  #
30
30
  class Stylesheet
31
+ FFI_PTR_SIZE = ::FFI.type_size(:pointer)
32
+ private_constant :FFI_PTR_SIZE
33
+
31
34
  # GC-managed compiled handle.
32
35
  class Handle < ::FFI::AutoPointer
33
36
  def self.release(ptr)
@@ -65,8 +68,22 @@ module Leptris::XML::XSLT
65
68
 
66
69
  # Apply to +document+ (not modified) and return the result tree
67
70
  # as an owning Document — query it with xpath/css like any other.
68
- def apply_to(document)
69
- raw = Leptris::XML::FFI.leptris_xslt_apply(@handle, document.c_ptr)
71
+ #
72
+ # params (#360): top-level xsl:param overrides as a Hash of
73
+ # name => value. Values bind as XPath STRINGS verbatim — the
74
+ # caller owns numeric/boolean quoting (libxslt conventions:
75
+ # pass '7' for a number, matching nokogiri's quote_params).
76
+ # Absent names keep the param's select/@default.
77
+ def apply_to(document, params: nil)
78
+ raw, buffer, = with_param_pairs(params) do |pairs, count|
79
+ if pairs
80
+ Leptris::XML::FFI.leptris_xslt_apply_params(
81
+ @handle, document.c_ptr, pairs, count)
82
+ else
83
+ Leptris::XML::FFI.leptris_xslt_apply(@handle, document.c_ptr)
84
+ end
85
+ end
86
+ buffer&.free
70
87
  if raw.null?
71
88
  raise Leptris::XML::XPathError,
72
89
  "transform failed: #{document.last_error || Leptris::XML::FFI.leptris_last_error}"
@@ -75,11 +92,40 @@ module Leptris::XML::XSLT
75
92
  end
76
93
 
77
94
  # Apply and serialize in one call — keeps top-level text nodes and
78
- # result fragments that the tree API would flatten.
79
- def serialize(document)
80
- str_ptr = Leptris::XML::FFI.leptris_xslt_apply_string(
81
- @handle, document.c_ptr)
95
+ # result fragments that the tree API would flatten. params: as
96
+ # #apply_to.
97
+ def serialize(document, params: nil)
98
+ str_ptr, buffer, = with_param_pairs(params) do |pairs, count|
99
+ if pairs
100
+ Leptris::XML::FFI.leptris_xslt_apply_string_params(
101
+ @handle, document.c_ptr, pairs, count)
102
+ else
103
+ Leptris::XML::FFI.leptris_xslt_apply_string(
104
+ @handle, document.c_ptr)
105
+ end
106
+ end
107
+ buffer&.free
82
108
  Leptris::XML::FFI.read_owned_string(str_ptr)
83
109
  end
110
+
111
+ # Flattens a params Hash into the engine's flat char** pairs
112
+ # array (borrowed for the C call's lifetime — allocated here,
113
+ # freed by the caller; the per-string MemoryPointers stay
114
+ # anchored until the call returns).
115
+ def with_param_pairs(params)
116
+ return yield(nil, 0) if params.nil? || params.empty?
117
+ unless params.is_a?(Hash)
118
+ raise ArgumentError, "params must be a Hash of name => value"
119
+ end
120
+
121
+ entries = params.keys.map(&:to_s)
122
+ .zip(params.values.map { |v| v.nil? ? "" : v.to_s })
123
+ strings = entries.flatten.map { |s| ::FFI::MemoryPointer.from_string(s) }
124
+ buffer = ::FFI::MemoryPointer.new(:pointer, strings.size)
125
+ strings.each_with_index do |ptr, i|
126
+ buffer.put_pointer(i * FFI_PTR_SIZE, ptr)
127
+ end
128
+ [yield(buffer, entries.size), buffer, strings]
129
+ end
84
130
  end
85
131
  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.285
4
+ - libleptris v1.9.290
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,79 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.290] - 2026-10-02
4
+
5
+ <!-- Edit this section with the actual release notes. -->
6
+ <!-- See https://keepachangelog.com for format guidance. -->
7
+
8
+ ### Added
9
+
10
+ - namespace forms for attribute plan rows — `ns_form`/`ns_uri` on `leptris_attr_plan` (#1486)
11
+ - Extends #1115's child-row pattern to attribute rows: `ns_form = 0` (the zero value) keeps the historical wire-name lookup byte-for-byte and existing aggregate initializers stay valid; `LEPTRIS_PLAN_NS_EXACT` + `ns_uri` makes `wire_name` the LOCAL name and binds the (namespace URI, local) identity through the element's in-scope prefix bindings — prefix-spelling-independent, so a `w14:paraId`-typed row binds whatever prefix the document uses for the Word 2010 URI. `LEPTRIS_PLAN_NS_ANY` matches the local name in any namespace; unprefixed attributes have no namespace and never bind EXACT (XML Namespaces §5.2).
12
+ - Unlocks the plan fast path for typed-attribute models: lutaml-model's `attr_type_ns` opt-out (one `w14:paraId` leaf cascading the WordprocessingML paragraph chain out of the plan path, against a measured 33% parse-CPU saving) can now emit ns-exact attribute rows instead.
13
+ - Build deep-copies `ns_uri`, zero-guards the trailing fields for hosts that `malloc` the spec without value-initialization (#1272 lesson), rejects `EXACT` without a URI as `INVALID_ARG`; free releases the copies. RED-first spec plus the full battery (1773/1773). (#1487)
14
+
15
+ ## [1.9.289] - 2026-10-02
16
+
17
+ ### Performance
18
+
19
+ - open-id compares for the scope/table-context stack walks (#1218 slice 6, tranche 2) (html)
20
+ - The scope walks (p-in-button-scope, button-in-scope, AFE `kn`), the table-context searches, and the table-context dispatch block compare `b->open_id[k]` integers instead of fetching the element name and running case-insensitive literal chains per stack slot. The AFE furthest-block scan deliberately keeps the name fetch — it runs mid-AFE between stack splices, where the id stack is not yet authoritative (webkit02:14). Interleaved A/B vs the tranche-1 tree: 48.2-53.7 -> 51.0-58.2 MB/s (+11% median) on the 2.6MB table-rows shape. (#1485)
21
+ - id-LUT membership for the foster/formatting/stack checks (#1218 slice 6) (html)
22
+ - The classifier table grows the 22 compared-but-unlisted names (HTML4 formatting set + MathML/SVG integration points, inert flags); `h_is_formatting` (14-way) and `h_fosterable` (15-way) become id-LUT lookups; 15 insertion-mode stack checks read `open_id[]` directly. 56.1 -> 59.0 MB/s (+5.2%) interleaved. Cumulative slice 6: ~+15% on the issue's fixture shape; the 3.4-7x deficit the issue reported at 1.9.203 is now a 1.3-1.55x LEAD over htmlReadMemory locally. (#1484)
23
+
24
+
25
+
26
+ ## [1.9.288] - 2026-10-02
27
+
28
+ ### Fixed
29
+
30
+ - top-level param values are XPath expressions (libxslt convention) — follow-up to #1478 (xslt)
31
+ - **Semantics correction to `leptris_xslt_apply_params` / `leptris_xslt_apply_string_params`, released briefly in 1.9.287:** value strings are now evaluated as XPath EXPRESSIONS (libxslt `params` convention; nokogiri's `quote_params` exists for exactly this), not bound as raw strings. Callers pass quoted literals — `{"n", "'7'"}` binds the string `7`; under 1.9.287 the quotes were part of the value. Numbers and source-derived expressions work (`7 * 6` → 42, `string(/r/@v)`), a later pair may reference an earlier one, a global `xsl:variable` may consume a caller value (§11 ordering), undeclared names bind into scope (libxslt parity), and a malformed expression fails the transform. Caller values bind before the globals run and a declared `xsl:param` default whose name a caller supplied never evaluates (§11.4). Specs ported from #1478 plus three RED-first expression cases; 1771/1771 and an external-consumer end-to-end probe. (#1481)
32
+
33
+
34
+
35
+ ## [1.9.287] - 2026-10-02
36
+
37
+ ### Added
38
+
39
+ - top-level param overrides — leptris_xslt_apply_params / _apply_string_params (leptris-ruby#360) (xslt)
40
+
41
+ ### Performance
42
+
43
+ - fused attr-loop dispatch on the shared chartype table (#1436) (parser)
44
+
45
+
46
+
47
+ ## [1.9.286] - 2026-10-02
48
+
49
+ ### Added
50
+
51
+ - top-level param overrides — leptris_xslt_apply_params / _apply_string_params (leptris-ruby#360) (xslt)
52
+
53
+ ### Performance
54
+
55
+ - fused attr-loop dispatch on the shared chartype table (#1436) (parser)
56
+
57
+
58
+
59
+ ## [1.9.286] - 2026-10-01
60
+
61
+ ### Fixed
62
+
63
+ - Door A skip flags must survive recorder reset and be accepted by sax_records (sax)
64
+ - `leptris_sax_recorder_reset()` rebuilt the parser without the creation-time `LEPTRIS_PARSE_SKIP_DUP_DETECTION` / `LEPTRIS_PARSE_SKIP_SOURCE_POSITIONS` bits, silently re-enabling both probes mid-stream: record counts diverged between the first pass and post-reset passes on the same document. The recorder now stores its flags and re-applies them on reset.
65
+ - `leptris_sax_records_parse()` rejected every nonzero flag, so the bulk-records lane could not opt out of either probe. The two Door A bits are now accepted (no-ops on the interleaved lane, which carries neither probe); unknown bits still return `LEPTRIS_ERROR_INVALID_ARG`.
66
+ - Regression specs: `Recorder.DoorAFlagsSurviveReset`, `SaxRecords.DoorAFlagsAccepted`. (#1472)
67
+
68
+ ### Changed
69
+
70
+ - element name stored as a self-relative int32 offset — `struct leptris_element` 48 → 44 bytes (#1285)
71
+ - The `char* name` was the struct's last 8-aligned member; replacing it with the tree-edge offset encoding drops the struct alignment to 4 and reclaims the tail padding.
72
+ - The dp parse lane packs element arrays at true size: peak RSS on a 15 MB / 400k-element document drops 253 → 239 MB (-5.4%) with parse and serialize times flat. Pool-allocated elements keep their stride (`LEPTRIS_POOL_ALIGNMENT` rounds up); ILP32 layouts are unchanged.
73
+ - Mutation/public-creator name stamps use the doc-tagged compact encoder (#1320 pattern) so far-mut-block spills register with the correct document. New specs: `ElementNameOffsetRoundTrip` plus a hard 44-byte size pin.
74
+
75
+
76
+
3
77
  ## [1.9.285] - 2026-10-01
4
78
 
5
79
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.285
6
+ VERSION 1.9.290
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -95,6 +95,18 @@ typedef struct {
95
95
  uint16_t predicate_count;
96
96
  uint16_t pad_pred;
97
97
  const leptris_attr_predicate* predicates;
98
+ /* #1486 (additive trailing fields, the #1115 child-row
99
+ * pattern): attribute-level namespace form. A row with
100
+ * ns_form set matches by (namespace URI, local name) —
101
+ * wire_name is the LOCAL name and the prefix spelling on the
102
+ * wire is irrelevant. NONE (0, the zero value) keeps the
103
+ * historical behavior: lookup by wire_name exactly as it
104
+ * appears on the wire (prefixed or bare). Unprefixed
105
+ * attributes have no namespace; EXACT requires a prefixed
106
+ * attribute whose prefix binds to ns_uri. */
107
+ uint8_t ns_form; /* LeptrisPlanNsForm */
108
+ uint8_t pad_ns;
109
+ const char* ns_uri; /* LEPTRIS_PLAN_NS_EXACT only */
98
110
  } leptris_attr_plan;
99
111
 
100
112
  typedef struct {
@@ -3096,7 +3096,6 @@ LEPTRIS_API void leptris_xslt_free(LeptrisXslt xslt);
3096
3096
  */
3097
3097
  LEPTRIS_API LeptrisDocument leptris_xslt_apply(LeptrisXslt xslt,
3098
3098
  LeptrisDocument doc);
3099
-
3100
3099
  /**
3101
3100
  * Apply a compiled stylesheet and serialize the result
3102
3101
  *
@@ -3111,6 +3110,50 @@ LEPTRIS_API LeptrisDocument leptris_xslt_apply(LeptrisXslt xslt,
3111
3110
  LEPTRIS_API char* leptris_xslt_apply_string(LeptrisXslt xslt,
3112
3111
  LeptrisDocument doc);
3113
3112
 
3113
+ /**
3114
+ * Apply a compiled stylesheet with caller-supplied top-level params
3115
+ *
3116
+ * Binds the given xsl:param values BEFORE the stylesheet's globals
3117
+ * run (XSLT §11.4): a caller value replaces the declared default,
3118
+ * unspecified params keep their defaults, and the value expressions
3119
+ * may reference the source document and earlier pairs.
3120
+ *
3121
+ * pairs is a flat name/value array — [name0, value0, name1,
3122
+ * value1, ...] — and count is the number of PAIRS (the array holds
3123
+ * 2*count entries). Each value is an XPath EXPRESSION evaluated
3124
+ * with the source document node as context; callers pre-quote
3125
+ * string literals ("'text'"), matching the libxslt convention.
3126
+ *
3127
+ * @param xslt Compiled stylesheet
3128
+ * @param doc Source document (not modified)
3129
+ * @param pairs Flat name/expression array, or NULL with count 0
3130
+ * @param count Number of name/expression pairs
3131
+ * @return Result tree (free with leptris_document_free), or NULL on
3132
+ * error — including a value expression that fails to
3133
+ * compile (message via leptris_document_last_error)
3134
+ */
3135
+ LEPTRIS_API LeptrisDocument leptris_xslt_apply_params(
3136
+ LeptrisXslt xslt, LeptrisDocument doc,
3137
+ const char* const* pairs, size_t count);
3138
+
3139
+ /**
3140
+ * Apply with caller-supplied top-level params and serialize
3141
+ *
3142
+ * leptris_xslt_apply_params + the leptris_xslt_apply_string
3143
+ * serialization contract (top-level text nodes and fragments
3144
+ * preserved).
3145
+ *
3146
+ * @param xslt Compiled stylesheet
3147
+ * @param doc Source document
3148
+ * @param pairs Flat name/expression array (see
3149
+ * leptris_xslt_apply_params), or NULL with count 0
3150
+ * @param count Number of name/expression pairs
3151
+ * @return Serialized result (free with leptris_free_string), or NULL
3152
+ */
3153
+ LEPTRIS_API char* leptris_xslt_apply_string_params(
3154
+ LeptrisXslt xslt, LeptrisDocument doc,
3155
+ const char* const* pairs, size_t count);
3156
+
3114
3157
  /**
3115
3158
  * Custom XPath function handler (string-valued).
3116
3159
  *
@@ -51,6 +51,10 @@ const uint8_t leptris_chartype_table[256] = {
51
51
  ['-']=CT_NAME, ['.']=CT_NAME,
52
52
  /* Whitespace */
53
53
  [' ']=CT_WS, ['\t']=CT_WS, ['\n']=CT_WS, ['\r']=CT_WS,
54
+ /* Attr-loop delimiters (#1436 fused scanner; see chartype.h) */
55
+ ['>']=CT_ATTR_GT, ['/']=CT_ATTR_SLASH,
56
+ ['=']=CT_ATTR_EQ,
57
+ ['"']=CT_ATTR_QUOTE, ['\'']=CT_ATTR_QUOTE,
54
58
  /* UTF-8 multibyte lead and continuation bytes — every byte
55
59
  * >= 0x80 is CT_UTF8 (lenient name scanning, <café>). */
56
60
  #define CT_U8(n) [n]=CT_UTF8
@@ -24,6 +24,16 @@
24
24
  * Matches flat_parser's lenient UTF-8
25
25
  * fallback (c >= 0xC0 for start, c >= 0x80
26
26
  * for continuation). */
27
+ /* Attr-loop delimiter bits (#1436 fused scanner). The open-tag/
28
+ * attribute loop dispatches on ONE table load: the whitespace skip
29
+ * itself yields the stop byte's class — tag close, self-close, '=',
30
+ * or the value quote (pugixml ct_parse_attr's combined-mask shape).
31
+ * These five bytes carry none of the flags above, so every existing
32
+ * mask is unaffected. */
33
+ #define CT_ATTR_GT 0x10 /* '>' — end of the open tag */
34
+ #define CT_ATTR_SLASH 0x20 /* '/' — self-close (with '>' lookahead) */
35
+ #define CT_ATTR_EQ 0x40 /* '=' — name/value separator */
36
+ #define CT_ATTR_QUOTE 0x80 /* '"', '\'' — value delimiters */
27
37
 
28
38
  /* The shared table. Defined once in chartype.c — fully static and
29
39
  * const, including the CT_UTF8 entries for bytes >= 0x80 (no
@@ -246,6 +246,7 @@ static void dp_plan_free(LeptrisPlan p) {
246
246
  dp_plan* d = &p->plans[i];
247
247
  for (uint32_t a = 0; a < d->attribute_count; a++) {
248
248
  free((char *)d->attribute_plans[a].wire_name);
249
+ free((char *)d->attribute_plans[a].ns_uri);
249
250
  for (uint16_t pi = 0;
250
251
  pi < d->attribute_plans[a].predicate_count; pi++) {
251
252
  free((char*)d->attribute_plans[a]
@@ -332,6 +333,32 @@ LEPTRIS_API LeptrisPlan leptris_plan_build(const leptris_plan_spec* spec,
332
333
  d->attribute_plans[a].predicate_count = 0;
333
334
  d->attribute_plans[a].pad_pred = 0;
334
335
  }
336
+ /* #1486 additive-ABI guard (the #1272 lesson): a
337
+ * host that mallocs the spec leaves the trailing
338
+ * ns fields garbage — normalize unset forms so the
339
+ * walker never reads a garbage pointer, and deep-
340
+ * copy EXACT uris. EXACT without a uri cannot
341
+ * express an identity: reject the spec. */
342
+ if (!s->attribute_plans[a].ns_form) {
343
+ d->attribute_plans[a].ns_form = 0;
344
+ d->attribute_plans[a].pad_ns = 0;
345
+ d->attribute_plans[a].ns_uri = NULL;
346
+ } else {
347
+ d->attribute_plans[a].pad_ns = 0;
348
+ if (s->attribute_plans[a].ns_form ==
349
+ LEPTRIS_PLAN_NS_EXACT) {
350
+ if (!s->attribute_plans[a].ns_uri) {
351
+ if (status) *status = LEPTRIS_ERROR_INVALID_ARG;
352
+ dp_plan_free(p);
353
+ return NULL;
354
+ }
355
+ d->attribute_plans[a].ns_uri =
356
+ dp_strdup(s->attribute_plans[a].ns_uri);
357
+ if (!d->attribute_plans[a].ns_uri) goto oom;
358
+ } else {
359
+ d->attribute_plans[a].ns_uri = NULL;
360
+ }
361
+ }
335
362
  if (s->attribute_plans[a].wire_name) {
336
363
  d->attribute_plans[a].wire_name =
337
364
  dp_strdup(s->attribute_plans[a].wire_name);
@@ -491,6 +518,50 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
491
518
  const LeptrisPlan pool,
492
519
  struct leptris_plan_result* out);
493
520
 
521
+ /* #1486 attribute-row binding. ns_form 0 = the historical
522
+ * wire-name lookup (the string exactly as it appears on the
523
+ * wire). A set form matches (namespace, local): the row's
524
+ * wire_name is the LOCAL name; each attribute's prefix resolves
525
+ * through the ELEMENT's in-scope bindings, so the identity holds
526
+ * across prefix spellings. Unprefixed attributes have no
527
+ * namespace (XML namespaces §5.2) — EXACT never binds them. */
528
+ static const char* dp_attr_value(LeptrisElement elem,
529
+ const leptris_attr_plan* ap) {
530
+ if (!ap->ns_form)
531
+ return leptris_element_attribute(elem, ap->wire_name);
532
+ for (LeptrisAttribute at = leptris_element_first_attribute(elem);
533
+ at; at = leptris_attribute_next(at)) {
534
+ const char* qn = leptris_attribute_get_name(at);
535
+ if (!qn) continue;
536
+ const char* colon = strchr(qn, ':');
537
+ const char* local = colon ? colon + 1 : qn;
538
+ if (strcmp(local, ap->wire_name) != 0) continue;
539
+ const char* uri = NULL;
540
+ if (colon) {
541
+ char prefix[128];
542
+ size_t plen = (size_t)(colon - qn);
543
+ if (plen >= sizeof(prefix)) continue;
544
+ memcpy(prefix, qn, plen);
545
+ prefix[plen] = 0;
546
+ uri = leptris_element_namespace_for_prefix(elem, prefix);
547
+ }
548
+ switch (ap->ns_form) {
549
+ case LEPTRIS_PLAN_NS_NONE:
550
+ if (uri) continue;
551
+ break;
552
+ case LEPTRIS_PLAN_NS_EXACT:
553
+ if (!uri || !ap->ns_uri ||
554
+ strcmp(uri, ap->ns_uri) != 0)
555
+ continue;
556
+ break;
557
+ default: /* ANY */
558
+ break;
559
+ }
560
+ return leptris_attribute_get_value(elem, at);
561
+ }
562
+ return NULL;
563
+ }
564
+
494
565
  static struct leptris_plan_result* dp_walk_element(LeptrisElement elem,
495
566
  const dp_plan* plan,
496
567
  const LeptrisPlan pool) {
@@ -510,7 +581,7 @@ static struct leptris_plan_result* dp_walk_element(LeptrisElement elem,
510
581
  for (uint32_t a = 0; a < plan->attribute_count; a++) {
511
582
  const leptris_attr_plan* ap = &plan->attribute_plans[a];
512
583
  if (!ap->wire_name) continue;
513
- const char* val = leptris_element_attribute(elem, ap->wire_name);
584
+ const char* val = dp_attr_value(elem, ap);
514
585
  if (!val) continue;
515
586
  v->attr_names[v->attr_count] = dp_strdup(ap->wire_name);
516
587
  v->attr_values[v->attr_count] = dp_strdup(val);