leptris 1.9.311.3-ppc64le-linux → 1.9.312.1-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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/Rakefile +1 -1
  4. data/lib/leptris/version.rb +1 -1
  5. data/lib/leptris/xml/builder.rb +169 -0
  6. data/lib/leptris/xml/descriptor.rb +42 -1
  7. data/lib/leptris/xml/document.rb +14 -0
  8. data/lib/leptris/xml/element.rb +11 -0
  9. data/lib/leptris/xml/ffi.rb +28 -3
  10. data/lib/leptris/xml/plan_value.rb +7 -0
  11. data/lib/leptris/xml.rb +1 -0
  12. data/lib/libleptris.so +0 -0
  13. data/vendor-src/README.md +1 -1
  14. data/vendor-src/libleptris/CHANGELOG.md +45 -0
  15. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  16. data/vendor-src/libleptris/bindings/rust/descriptor/src/lib.rs +14 -0
  17. data/vendor-src/libleptris/bindings/rust/tests/descriptor_abi.rs +5 -0
  18. data/vendor-src/libleptris/src/include/leptris/descriptor.h +45 -1
  19. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +529 -23
  20. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +50 -0
  21. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +736 -1
  22. data/vendor-src/libleptris/test/abi/test_dom_mutation_bugs.cpp +67 -0
  23. data/vendor-src/libleptris/test/perf/test_perf.cpp +7 -3
  24. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-111.xml +17 -17
  25. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-111.xsl +45 -45
  26. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-115.xsl +19 -19
  27. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-156.imp1.imp +16 -16
  28. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-156.imp2.imp +10 -10
  29. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-156.xsl +12 -12
  30. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-165.xml +10 -10
  31. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-80.xml +6 -6
  32. data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-80.xsl +29 -29
  33. data/vendor-src/libleptris/vcpkg.json +1 -1
  34. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffe8f4a4632aa5c0433127b27a2fe02fd9f6e662f50ea2e0302eb639c01a3823
4
- data.tar.gz: 59459801b12745b0c4260fcad1c9343bfa1c72e474c280617b0fc3395af7035c
3
+ metadata.gz: 8be66f684edb1d18e35ab05b6926becf591022c5c776c8fb27c2bf7fb9c6578c
4
+ data.tar.gz: caf2d6f092fcc16dfe7931b8372fea55f244186a512612552f2bac89435e320c
5
5
  SHA512:
6
- metadata.gz: cc5d3214f664abfb6a43efe6b646d4b5b46bbde230aca662e5c1d87781c7990d0c82147f4ce3ade2eca4fa3da05ee78ae2ccab939c88f80bc8fe79de03243369
7
- data.tar.gz: 731ed271acf74deb3c25f09cb70825ad7aed0f6178c73de1edb9c0a8576ddaf689cadb4e627bd792d1c3ca621f930023483a043df31f205905655de28f044ad3
6
+ metadata.gz: be6b60f3574628511bc8b304865be8462b0ba8b1089333e507fe28f1806b7fda751877fd8fa2e263dc0755dec5c37f2bc4305660988d90aa99b8c02d713077c6
7
+ data.tar.gz: 2ca1422057b28d7cfbeb8422f5ab7ec35047d07dca0fe5ae8098358984e80c80486c1bc3ffb489e8a91fce2625dcbf54611feb0f3a552cc6ef637e166ded9684
data/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ ## [1.9.312.1] - 2026-10-07
2
+
3
+ ### Added
4
+
5
+ - **Document#build / Element#build — the builder face (#374 lever
6
+ 2)**: markup-accumulating construction. The block turns into
7
+ well-formed markup in Ruby (method_missing element dispatch
8
+ with attrs/text, `#text`/`#cdata`/`#comment`/`#raw`, one-pass
9
+ escaping of `& < > "`), and the flush performs ONE native
10
+ crossing for the whole subtree — parse + `root=` adoption for
11
+ document sinks (exactly one root, the parser enforces it;
12
+ returns the installed root wrapper), `append_markup` for
13
+ element sinks (returns the added NodeSet). Measured DSL-vs-DSL
14
+ under load (interleaved CPU-time medians, build(500)):
15
+ leptris builder 2.68 ms vs nokogiri's builder 5.29 ms —
16
+ **~2.0x** — and ~1.2x over our own object face (3.19 ms,
17
+ itself ~1.7x nokogiri after the 1.9.311.3 trim).
18
+
19
+ ## [1.9.312.0] - 2026-10-07
20
+
21
+ ### Added — engine sync (libleptris 1.9.312)
22
+
23
+ - **Descriptor#serialize — plan-guided serialization (#1551)**:
24
+ `leptris_plan_serialize` round-trips a walk result back to XML —
25
+ the plan supplies element wrappers (row wire_name + the new
26
+ `ns_prefix:` field on element/child/attr rows), the result the
27
+ content. Children emit in document order; rows with ns EXACT +
28
+ `ns_prefix:` emit prefixed names and every distinct (prefix,
29
+ uri) pair is declared exactly once, on the output root, in
30
+ first-encounter order; text/attr values escape; RAW members are
31
+ verbatim. The three plan structs take the additive trailing
32
+ `ns_prefix` field (attr row 40 → 48B; the row-size skew gate
33
+ holds). `PlanValue#result_ptr` exposes the whole-result handle
34
+ the face reads.
35
+ - **Wildcard child rows (#1552)**: `kind: :wildcard` binds every
36
+ element child no named sibling row bound — named rows win
37
+ regardless of row order; the row emits exactly one COLLECTION
38
+ (echoing wire_name/type_tag, even when empty); members keep
39
+ document order; an explicit `ns:` filters the remainder
40
+ (`ns: :none` binds no-namespace children alone; unset stays the
41
+ catch-all ANY default). PLAN_KIND_WILDCARD = 7.
42
+
43
+ ### Fixed — engine sync (1.9.312)
44
+
45
+ - **Root-level leaf splices adopt single-node (#1539)**: a leaf
46
+ directly on a scratch document's children chain (prolog
47
+ comment / PI) no longer reports LEPTRIS_ERROR_MEMORY on
48
+ cross-document append — spec-pinned in the adoption suite.
49
+
1
50
  ## [1.9.311.3] - 2026-10-06
2
51
 
3
52
  ### Performance
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.311"
11
+ LIBLEPTRIS_VERSION = "1.9.312"
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.311.3"
4
+ VERSION = "1.9.312.1"
5
5
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Markup-accumulating subtree construction (#374 lever 2): the
4
+ # builder turns a Ruby block into well-formed markup in a plain
5
+ # String (Ruby-side cost only), and the flush performs ONE native
6
+ # crossing — parse + attach — for the whole subtree. The
7
+ # per-element FFI crossing that dominated the object-face build
8
+ # profile collapses to one per #build call.
9
+ #
10
+ # doc.build do |b|
11
+ # b.catalog(id: "c") do
12
+ # b.item(id: 7) { b.name "Item 7" }
13
+ # end
14
+ # end
15
+ #
16
+ # Element names arriving through method_missing are identifier-
17
+ # shaped by construction (Ruby method names cannot carry markup
18
+ # metacharacters); the explicit #element path checks NAME_RE.
19
+ # Text and attribute values escape in one pass per string.
20
+ class Leptris::XML::Builder
21
+ NAME_RE = /\A[a-zA-Z_][\w.-]*\z/
22
+
23
+ ESCAPE = {
24
+ "&" => "&amp;",
25
+ "<" => "&lt;",
26
+ ">" => "&gt;",
27
+ '"' => "&quot;",
28
+ }.freeze
29
+ ESCAPE_RE = /[&<>"]/
30
+ private_constant :ESCAPE, :ESCAPE_RE
31
+
32
+ # +sink+ receives the flushed markup (a Document or Element).
33
+ def initialize(sink)
34
+ @sink = sink
35
+ @buf = +""
36
+ @depth = 0
37
+ end
38
+
39
+ def element(name, attributes = nil)
40
+ name = name.to_s
41
+ unless name =~ NAME_RE
42
+ raise ArgumentError,
43
+ "element name must match #{NAME_RE.inspect}, got #{name.inspect}"
44
+ end
45
+ @buf << "<" << name
46
+ write_attributes(attributes) if attributes
47
+ if block_given?
48
+ @buf << ">"
49
+ @depth += 1
50
+ begin
51
+ yield self
52
+ ensure
53
+ @depth -= 1
54
+ end
55
+ @buf << "</" << name << ">"
56
+ else
57
+ @buf << "/>"
58
+ end
59
+ self
60
+ end
61
+
62
+ def text(content)
63
+ @buf << escape_text(content.to_s)
64
+ self
65
+ end
66
+
67
+ def cdata(content)
68
+ @buf << "<![CDATA[" << content.to_s << "]]>"
69
+ self
70
+ end
71
+
72
+ def comment(content)
73
+ @buf << "<!--" << escape_text(content.to_s) << "-->"
74
+ self
75
+ end
76
+
77
+ # Verbatim markup — the escape hatch for pre-serialized
78
+ # fragments; the caller owns well-formedness.
79
+ def raw(markup)
80
+ @buf << markup.to_s
81
+ self
82
+ end
83
+
84
+ def method_missing(name, *args, &block)
85
+ # Method names are identifier-shaped: they cannot carry <, >,
86
+ # &, quotes, or whitespace, so they need no NAME_RE round-trip
87
+ # (trailing = / ? / ! are legal Ruby but never legal XML
88
+ # name tails here — reject them explicitly).
89
+ return super if name.to_s.end_with?("=", "?", "!")
90
+ case args.length
91
+ when 0
92
+ element(name.to_s, &block)
93
+ when 1
94
+ arg = args[0]
95
+ if arg.is_a?(Hash)
96
+ element(name.to_s, arg, &block)
97
+ else
98
+ element(name.to_s) { |b| b.text(arg); block&.call(b) }
99
+ end
100
+ when 2
101
+ attrs, txt = args
102
+ return super unless attrs.is_a?(Hash)
103
+ element(name.to_s, attrs) { |b| b.text(txt); block&.call(b) }
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ def respond_to_missing?(name, include_private = false)
110
+ !name.to_s.end_with?("=", "?", "!") || super
111
+ end
112
+
113
+ # One native crossing for the accumulated subtree: the flush
114
+ # parses + attaches. Document sinks take exactly one root
115
+ # (set_root_ex adopts the parsed root); element sinks append
116
+ # every fragment child.
117
+ def flush
118
+ unless @depth.zero?
119
+ raise Leptris::XML::Error,
120
+ "unbalanced builder — unclosed elements remain (depth #{@depth})"
121
+ end
122
+ markup = @buf
123
+ return @sink if markup.empty?
124
+ case @sink
125
+ when Leptris::XML::Document
126
+ parsed = Leptris::XML::Document.parse(markup)
127
+ root = parsed.root
128
+ if root.nil?
129
+ raise Leptris::XML::Error,
130
+ "builder markup produced no root element"
131
+ end
132
+ @sink.root = root
133
+ @buf = +""
134
+ # The INSTALLED root — the same wrapper #root now yields
135
+ # (root= returns the parsed source, not the adoption copy).
136
+ @sink.root
137
+ when Leptris::XML::Element
138
+ added = @sink.add_child(markup)
139
+ @buf = +""
140
+ added
141
+ else
142
+ raise ArgumentError,
143
+ "builder sink must be a Document or Element, got #{@sink.class}"
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def write_attributes(attributes)
150
+ case attributes
151
+ when Hash
152
+ attributes.each do |k, v|
153
+ @buf << " " << k.to_s << '="' << escape_attr(v) << '"'
154
+ end
155
+ when nil then # nothing
156
+ else
157
+ raise ArgumentError,
158
+ "attributes must be a Hash, got #{attributes.class}"
159
+ end
160
+ end
161
+
162
+ def escape_text(value)
163
+ value.gsub(ESCAPE_RE, ESCAPE)
164
+ end
165
+
166
+ def escape_attr(value)
167
+ value.to_s.gsub(ESCAPE_RE, ESCAPE)
168
+ end
169
+ end
@@ -74,6 +74,12 @@ class Leptris::XML::Descriptor
74
74
  raw: Leptris::XML::FFI::PLAN_KIND_RAW,
75
75
  content: Leptris::XML::FFI::PLAN_KIND_CONTENT,
76
76
  callback: Leptris::XML::FFI::PLAN_KIND_CALLBACK,
77
+ # libleptris 1.9.312 (#1552): the remainder bucket — binds
78
+ # every element child no named sibling row bound. Emits one
79
+ # COLLECTION (even when empty); named rows win regardless of
80
+ # row order. child_plan_index >= 0 walks members through that
81
+ # plan; -1 (the default) emits RAW serialized subtrees.
82
+ wildcard: Leptris::XML::FFI::PLAN_KIND_WILDCARD,
77
83
  }.freeze
78
84
  private_constant :KINDS
79
85
 
@@ -177,6 +183,10 @@ class Leptris::XML::Descriptor
177
183
  end
178
184
  ep[:ns_form] = ns_form
179
185
  ep[:ns_uri] = ns_uri
186
+ # libleptris 1.9.312 (#1551): the root plan's serialization
187
+ # prefix (ns_form EXACT rows emit "prefix:local" and the
188
+ # xmlns declaration lands once on the output root).
189
+ ep[:ns_prefix] = anchor_string(anchors, plan[:ns_prefix]) if plan[:ns_prefix]
180
190
  attrs = plan[:attributes] || []
181
191
  attr_memory = attrs.empty? ? nil :
182
192
  ::FFI::MemoryPointer.new(Leptris::XML::FFI::AttrPlan, attrs.size)
@@ -198,6 +208,9 @@ class Leptris::XML::Descriptor
198
208
  ap[:ns_uri] =
199
209
  row[:ns].is_a?(Hash) ?
200
210
  anchor_string(anchors, row[:ns].fetch(:exact)) : nil
211
+ # libleptris 1.9.312 (#1551): serialization prefix — with
212
+ # ns_form EXACT the attribute emits as "prefix:local".
213
+ ap[:ns_prefix] = anchor_string(anchors, row[:ns_prefix]) if row[:ns_prefix]
201
214
  pack_predicates(ap, row, anchors)
202
215
  end
203
216
  ep[:attribute_count] = attrs.size
@@ -214,16 +227,24 @@ class Leptris::XML::Descriptor
214
227
  cp[:child_plan_index] = row[:child_plan_index] || -1
215
228
  # Rule-level ns form (libleptris 1.9.178, #1115): siblings
216
229
  # under one parent can require different URIs when set;
217
- # defaults to NONE for backward compatibility.
230
+ # defaults to NONE for backward compatibility. WILDCARD
231
+ # rows (#1552) read an explicit :ns through pad0 (an
232
+ # explicit :none filters no-namespace remainder alone;
233
+ # unset stays the catch-all ANY default).
218
234
  cp[:ns_form] =
219
235
  case row[:ns]
220
236
  when :any then Leptris::XML::FFI::PLAN_NS_ANY
221
237
  when Hash then Leptris::XML::FFI::PLAN_NS_EXACT
222
238
  else Leptris::XML::FFI::PLAN_NS_NONE
223
239
  end
240
+ cp[:pad0] = 1 if row[:kind] == :wildcard && row.key?(:ns)
224
241
  cp[:ns_uri] =
225
242
  row[:ns].is_a?(Hash) ?
226
243
  anchor_string(anchors, row[:ns].fetch(:exact)) : nil
244
+ # libleptris 1.9.312 (#1551): serialization prefix — with
245
+ # ns_form EXACT the element emits as "prefix:local" and
246
+ # its declaration lands once on the output root.
247
+ cp[:ns_prefix] = anchor_string(anchors, row[:ns_prefix]) if row[:ns_prefix]
227
248
  pack_predicates(cp, row, anchors)
228
249
  end
229
250
  ep[:child_count] = children.size
@@ -318,6 +339,26 @@ class Leptris::XML::Descriptor
318
339
  owner: true, plans: @plans, plan: @plans[0])
319
340
  end
320
341
 
342
+ # libleptris 1.9.312 (#1551): serialize a walk result back to
343
+ # XML guided by this plan — the plan supplies element wrappers
344
+ # (row wire_name + ns_prefix), the result the content; children
345
+ # emit in document order. Rows with ns_form EXACT + ns_prefix
346
+ # emit prefixed names and every distinct (prefix, uri) pair is
347
+ # declared exactly once, on the output root, first-encounter
348
+ # order. Text and attribute values are escaped; RAW members are
349
+ # verbatim.
350
+ def serialize(plan_value)
351
+ result_ptr = plan_value.result_ptr
352
+ status = ::FFI::MemoryPointer.new(:int)
353
+ raw = Leptris::XML::FFI.leptris_plan_serialize(
354
+ @handle, result_ptr, status)
355
+ if raw.null?
356
+ raise Leptris::XML::Error,
357
+ "plan serialize failed (status=#{status.read_int})"
358
+ end
359
+ Leptris::XML::FFI.read_owned_string(raw)
360
+ end
361
+
321
362
  # The fused loop (#1269b): source bytes → typed rows in ONE C
322
363
  # call — leptris_plan_materialize runs parse, walk, and free
323
364
  # natively; the returned PlanValue tree is standalone and the
@@ -232,6 +232,20 @@ class Leptris::XML::Document
232
232
  wrap(raw)
233
233
  end
234
234
 
235
+ # Markup-accumulating construction (#374 lever 2): the block
236
+ # accumulates well-formed markup in Ruby; the flush performs
237
+ # ONE native crossing for the whole subtree. Exactly one root
238
+ # element — the parsed root is adopted with set_root_ex.
239
+ # Returns the flush's value (the installed root element).
240
+ #
241
+ # doc = Leptris::XML::Document.create
242
+ # doc.build { |b| b.catalog(id: "c") { b.item { b.name "x" } } }
243
+ def build
244
+ builder = Leptris::XML::Builder.new(self)
245
+ yield builder
246
+ builder.flush
247
+ end
248
+
235
249
  # Deep copy of +node+ in a NEW document — the one authority
236
250
  # behind Node#dup / Element#dup and the indent-unit path.
237
251
  # Elements deep-copy through the C copier (every child kind and
@@ -665,6 +665,17 @@ class Leptris::XML::Element < Leptris::XML::Node
665
665
  Leptris::XML::Node.wrap_fresh(ptr, @document, Leptris::XML::FFI::NODE_ELEMENT)
666
666
  end
667
667
 
668
+ # Markup-accumulating child construction (#374 lever 2): the
669
+ # block accumulates well-formed child markup in Ruby; the flush
670
+ # appends every fragment child in ONE native crossing
671
+ # (append_markup). Returns the flush's value (the NodeSet of
672
+ # added children).
673
+ def build
674
+ builder = Leptris::XML::Builder.new(self)
675
+ yield builder
676
+ builder.flush
677
+ end
678
+
668
679
  def add_child(node_or_markup)
669
680
  case node_or_markup
670
681
  when Leptris::XML::Node
@@ -306,7 +306,12 @@ module Leptris
306
306
  # unset = the historical wire-name lookup.
307
307
  :ns_form, :uint8,
308
308
  :pad_ns, :uint8,
309
- :ns_uri, :pointer
309
+ :ns_uri, :pointer,
310
+ # libleptris 1.9.312 (#1551): intended wire prefix
311
+ # for plan-path serialization (ns_form EXACT emits
312
+ # "ns_prefix:wire_name"). NULL keeps matching-only
313
+ # behavior.
314
+ :ns_prefix, :pointer
310
315
  end
311
316
  class ChildPlan < ::FFI::Struct
312
317
  layout :wire_name, :pointer,
@@ -321,7 +326,13 @@ module Leptris
321
326
  # #1272: element-side predicates (same semantics).
322
327
  :predicate_count, :uint16,
323
328
  :pad_pred, :uint16,
324
- :predicates, :pointer
329
+ :predicates, :pointer,
330
+ # libleptris 1.9.312 (#1551): serialization prefix,
331
+ # same contract as AttrPlan's. For WILDCARD rows
332
+ # (libleptris 1.9.312, #1552) pad0 marks an EXPLICIT
333
+ # ns_form (0 then filters no-namespace remainder);
334
+ # unset keeps the catch-all ANY default.
335
+ :ns_prefix, :pointer
325
336
  end
326
337
  class ElementPlan < ::FFI::Struct
327
338
  layout :element_name, :pointer,
@@ -333,7 +344,10 @@ module Leptris
333
344
  :attribute_plans, :pointer,
334
345
  :child_plans, :pointer,
335
346
  :flags, :uint16,
336
- :pad1, :uint16
347
+ :pad1, :uint16,
348
+ # libleptris 1.9.312 (#1551): the root plan's
349
+ # serialization prefix.
350
+ :ns_prefix, :pointer
337
351
  end
338
352
  class PlanSpec < ::FFI::Struct
339
353
  layout :abi_version, :uint32,
@@ -349,6 +363,11 @@ module Leptris
349
363
  PLAN_KIND_RAW = 4
350
364
  PLAN_KIND_CONTENT = 5
351
365
  PLAN_KIND_CALLBACK = 6
366
+ # libleptris 1.9.312 (#1552): catch-all row — binds every
367
+ # element child no named sibling row bound (named rows win
368
+ # regardless of order); emits one COLLECTION echoing the
369
+ # row's wire_name/type_tag, members in document order.
370
+ PLAN_KIND_WILDCARD = 7
352
371
 
353
372
  PLAN_FLAG_MIXED_CONTENT = 0x1
354
373
  PLAN_FLAG_ORDERED = 0x2
@@ -391,6 +410,12 @@ module Leptris
391
410
  :leptris_plan_result
392
411
  attach_function :leptris_plan_result_free,
393
412
  [:leptris_plan_result], :void
413
+ # libleptris 1.9.312 (#1551): serialize a walk result back
414
+ # to XML guided by its plan — the plan supplies element
415
+ # wrappers (row wire_name + ns_prefix), the result the
416
+ # content. Caller-freed string (leptris_free_string).
417
+ attach_function :leptris_plan_serialize,
418
+ [:leptris_plan, :leptris_plan_result, :pointer], :pointer
394
419
  attach_function :leptris_plan_value_kind,
395
420
  [:leptris_plan_result], :int
396
421
  attach_function :leptris_plan_value_name,
@@ -30,6 +30,13 @@ class Leptris::XML::PlanValue
30
30
  @counter = counter || [0]
31
31
  end
32
32
 
33
+ # The engine value handle — the ROOT value's is the whole-result
34
+ # handle whole-result faces (leptris_plan_serialize) read. Pass
35
+ # the value Descriptor#walk/#materialize returned.
36
+ def result_ptr
37
+ @ptr
38
+ end
39
+
33
40
  def kind
34
41
  @counter[0] += 1
35
42
  VALUE_KINDS.fetch(Leptris::XML::FFI.leptris_plan_value_kind(@ptr))
data/lib/leptris/xml.rb CHANGED
@@ -14,6 +14,7 @@ module Leptris
14
14
  autoload :Namespace, "leptris/xml/namespace"
15
15
  autoload :Document, "leptris/xml/document"
16
16
  autoload :DocumentFragment, "leptris/xml/document_fragment"
17
+ autoload :Builder, "leptris/xml/builder"
17
18
  autoload :DocType, "leptris/xml/doc_type"
18
19
  autoload :NodeSet, "leptris/xml/node_set"
19
20
  autoload :Searchable, "leptris/xml/searchable"
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.311
4
+ - libleptris v1.9.312
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,50 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.312] - 2026-10-06
4
+
5
+ ### Added
6
+
7
+ - **Plan-path serialization emits namespace declarations (#1551,
8
+ PR #1558).** New `leptris_plan_serialize(plan, result)`: the plan
9
+ supplies element wrappers, the result the content — children emit
10
+ in document order, collections are transparent, RAW members
11
+ verbatim, text and attributes escaped. Rows carrying
12
+ `ns_form = EXACT` plus the new trailing `ns_prefix` field emit
13
+ prefixed wire names, and every distinct (prefix, uri) pair is
14
+ declared exactly once on the output root in first-encounter
15
+ order. `leptris_attr_plan` takes the same field, so #1486-matched
16
+ attributes emit the plan-chosen spelling. Byte-parity gates on a
17
+ `w:document` corpus; additive trailing fields keep
18
+ `PLAN_ABI_VERSION` at 1 (64-bit attr row 40 -> 48 bytes). Rust
19
+ mirror (`leptris-descriptor`) updated in lockstep.
20
+ - **Wildcard (catch-all) child rows (#1552, PR #1555).**
21
+ `LEPTRIS_PLAN_KIND_WILDCARD` binds every element child no named
22
+ sibling row bound — named rows take precedence regardless of row
23
+ order (a two-pass walk over a completed `bound[]` map; a named
24
+ row that rejected a child does not reserve it). Namespace-form
25
+ aware (`pad0` marks the form explicit so `NS_NONE` is
26
+ expressible; unset keeps the catch-all ANY default), emits one
27
+ COLLECTION echoing the row wire_name/type_tag even when empty,
28
+ members in document order; `child_plan_index >= 0` walks members
29
+ through that plan. Rust mirror gains `PLAN_KIND_WILDCARD = 7`.
30
+
31
+ ### Fixed
32
+
33
+ - **Root-level leaf splice arguments adopt single-node (#1539,
34
+ PR #1554).** A leaf directly on a scratch document's children
35
+ chain (prolog/epilog comment or PI) reported
36
+ `LEPTRIS_ERROR_MEMORY` on cross-document append: the chain
37
+ copier hit the root element and misread it as OOM. The seam now
38
+ copies exactly the argument for the doc-level shape; in-element
39
+ leaves keep the #540 chain shape.
40
+ - **Emulated-i386 perf-gate stability (PR #1561).** The
41
+ indexed-child-access shape budget widens 6x -> 7.5x: the qemu
42
+ timer noise of the ILP32 leg flaked past 6x three times in one
43
+ day while every native leg measured ~3x. Linear ~3x, quadratic
44
+ ~9x, O(N^3) ~27x — the gate keeps its discrimination.
45
+
46
+
47
+
3
48
  ## [1.9.311] - 2026-10-06
4
49
 
5
50
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.311
6
+ VERSION 1.9.312
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -31,6 +31,11 @@ pub const PLAN_KIND_NESTED: PlanKind = 3;
31
31
  pub const PLAN_KIND_RAW: PlanKind = 4;
32
32
  pub const PLAN_KIND_CONTENT: PlanKind = 5;
33
33
  pub const PLAN_KIND_CALLBACK: PlanKind = 6;
34
+ /// #1552: catch-all row — binds every element child no named
35
+ /// sibling row bound; emits one COLLECTION echoing the row
36
+ /// wire_name/type_tag; `child_plan_index >= 0` walks members
37
+ /// through that plan, else members are RAW serialized subtrees.
38
+ pub const PLAN_KIND_WILDCARD: PlanKind = 7;
34
39
 
35
40
  /// Element plan flags.
36
41
  pub const PLAN_FLAG_MIXED_CONTENT: u32 = 0x1;
@@ -83,6 +88,9 @@ pub struct AttrPlan {
83
88
  pub pad_ns: u8,
84
89
  /// `PLAN_NS_EXACT` only.
85
90
  pub ns_uri: *const c_char,
91
+ /// #1551: intended wire prefix for serialized output
92
+ /// (`leptris_plan_serialize`); meaningful with EXACT.
93
+ pub ns_prefix: *const c_char,
86
94
  }
87
95
 
88
96
  /// Child binding row.
@@ -107,6 +115,9 @@ pub struct ChildPlan {
107
115
  pub predicate_count: u16,
108
116
  pub pad_pred: u16,
109
117
  pub predicates: *const AttrPredicate,
118
+ /// #1551: intended wire prefix for serialized output
119
+ /// (`leptris_plan_serialize`); meaningful with EXACT.
120
+ pub ns_prefix: *const c_char,
110
121
  }
111
122
 
112
123
  /// One element plan: the attribute and child rows bound to a name.
@@ -127,6 +138,9 @@ pub struct ElementPlan {
127
138
  /// `PLAN_FLAG_*`.
128
139
  pub flags: u16,
129
140
  pub pad1: u16,
141
+ /// #1551: the root plan's intended wire prefix for serialized
142
+ /// output (`leptris_plan_serialize`); meaningful with EXACT.
143
+ pub ns_prefix: *const c_char,
130
144
  }
131
145
 
132
146
  /// Top-level plan specification handed to the engine.
@@ -86,6 +86,7 @@ fn plan_build_and_walk_round_trip() {
86
86
  ns_form: descriptor::PLAN_NS_NONE,
87
87
  pad_ns: 0,
88
88
  ns_uri: ptr::null(),
89
+ ns_prefix: ptr::null(),
89
90
  };
90
91
  let child_row = descriptor::ChildPlan {
91
92
  wire_name: title.as_ptr(),
@@ -98,6 +99,7 @@ fn plan_build_and_walk_round_trip() {
98
99
  predicate_count: 0,
99
100
  pad_pred: 0,
100
101
  predicates: ptr::null(),
102
+ ns_prefix: ptr::null(),
101
103
  };
102
104
  let book_plan = descriptor::ElementPlan {
103
105
  element_name: book.as_ptr(),
@@ -110,6 +112,7 @@ fn plan_build_and_walk_round_trip() {
110
112
  child_plans: &child_row,
111
113
  flags: 0,
112
114
  pad1: 0,
115
+ ns_prefix: ptr::null(),
113
116
  };
114
117
  let root_child = descriptor::ChildPlan {
115
118
  wire_name: book.as_ptr(),
@@ -124,6 +127,7 @@ fn plan_build_and_walk_round_trip() {
124
127
  predicate_count: 0,
125
128
  pad_pred: 0,
126
129
  predicates: ptr::null(),
130
+ ns_prefix: ptr::null(),
127
131
  };
128
132
  let root_plan = descriptor::ElementPlan {
129
133
  element_name: catalog.as_ptr(),
@@ -136,6 +140,7 @@ fn plan_build_and_walk_round_trip() {
136
140
  child_plans: &root_child,
137
141
  flags: 0,
138
142
  pad1: 0,
143
+ ns_prefix: ptr::null(),
139
144
  };
140
145
  let plans = [root_plan, book_plan];
141
146
  let spec = descriptor::PlanSpec {