leptris 1.9.160.0 → 1.9.162.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2644772a66a1e58e517fe5b5e2b2fb417ac1592863f3de375d1659ecebfeaec6
4
- data.tar.gz: ee478cc9f2387ff6527a680c37817d1c650bfe50c0e9c891768784db396cc29c
3
+ metadata.gz: c862a7a722a5595a09c5f2507c0f27cfb17a15d5a3216147301b07dcde63d75e
4
+ data.tar.gz: 2e672c08d5f6149c8bbda83fa4bb9be56ca981a290172a8e9fb4835c86d6ea58
5
5
  SHA512:
6
- metadata.gz: 7e6dfa835f5704689bf7fa50bfac557bf4637b556b9f84f6ae437cf020bafe09cb42c866692d159da8f11ed0a8bad9439d13639f4c6380159d9d0c2a75600d42
7
- data.tar.gz: 33558c5c76bbc5130b52e4d4d2258f4918e9c101e18494772d29a55ad758ab089b98753d9ca6cea9cd0ca7ae09107d952c701d6dcdcbdb7557a0c3b13ea7e067
6
+ metadata.gz: 65bf38576bd8bd1873946b7944661b450a7f2ccc42e82eae2f875e8667cc2809cb634172d7ddaf997b5040273b3dfd7608b501be21f1c27a309fd4ac2c62c226
7
+ data.tar.gz: 24c01320b932f27ba2483c46777ffd7e16c841b19aaa2bf78c2dbddc982eb35b46d4b9d58bbb81754ee51cb40423cc1f0b20bce25688427d067f2db37f0a0673
data/CHANGELOG.md CHANGED
@@ -5,7 +5,31 @@ 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.160.0] - 2026-09-14
8
+ ## [1.9.162.0] - 2026-09-14
9
+
10
+ ### Added
11
+
12
+ - **`Leptris::XML::Descriptor`** (libleptris 1.9.162, upstream
13
+ #1039 — the tree-shaped schema-descriptor materialization ABI):
14
+ compile a plan tree once (`Descriptor.build` — Ruby hash DSL;
15
+ nested plans flatten to the engine's plan array), then
16
+ `#walk(element)` materializes a whole subtree in ONE native
17
+ pass — no per-element Ruby calls. Rows: :scalar / :collection /
18
+ :nested / :raw / :content (mixed-content text runs) / :callback
19
+ (value + byte position + type_tag echo); per-plan namespace
20
+ forms (:none/:any/{exact:}), flags (:mixed_content/:ordered/
21
+ :cdata/:ns_lenient). Results are lazy PlanValue trees that
22
+ OUTLIVE the document; `#to_ruby` materializes. ABI
23
+ version-checked at build. Audit 291 -> 306.
24
+ - **`Node#byte_offset`** — the parse-source byte position of the
25
+ node's markup (the position CALLBACK rows echo; 0 unknown).
26
+
27
+ ### Changed
28
+
29
+ - **Lockstep with libleptris 1.9.162** (1.9.160 -> 1.9.162):
30
+ NEON count3 deferred horizontal reduction (-33% on the 48 KB
31
+ copy+count pre-scan; parse rows gain ~1 us each) — 1.9.161.
32
+ \n## [1.9.160.0] - 2026-09-14
9
33
 
10
34
  ### Changed
11
35
 
data/README.adoc CHANGED
@@ -436,6 +436,45 @@ dispositions drop unmatched subtrees in the built-in initial
436
436
  descent (https://github.com/leptris/leptris/issues/705[#705]) —
437
437
  the other four dispositions are spec-correct. XQuery has no
438
438
  entry point yet.
439
+ == Descriptor materialization
440
+
441
+ Compile a schema descriptor once, then materialize a whole subtree
442
+ against it in ONE native pass — no per-element Ruby calls (frameworks
443
+ rebuilding typed models from XML; libleptris 1.9.162):
444
+
445
+ [source,ruby]
446
+ ----
447
+ descriptor = Leptris::XML::Descriptor.build(
448
+ name: "catalog",
449
+ attributes: [{ name: "version", kind: :scalar }],
450
+ children: [
451
+ { name: "item", kind: :nested, plan: {
452
+ name: "item",
453
+ attributes: [{ name: "id", kind: :scalar }],
454
+ children: [
455
+ { name: "name", kind: :scalar },
456
+ { name: "price", kind: :scalar },
457
+ { name: "opt", kind: :collection },
458
+ ] } },
459
+ ])
460
+
461
+ tree = descriptor.walk(doc.root).to_ruby
462
+ # { kind: :element, attributes: { "version" => "2.0" }, children: [
463
+ # { kind: :element, name: "item", attributes: { "id" => "1" },
464
+ # children: ["first", "1.99", ["a", "b"]] }, ...] }
465
+ ----
466
+
467
+ Row kinds: `:scalar`, `:collection`, `:nested` (via `plan:`), `:raw`
468
+ (serialized subtree), `:content` (mixed-content text runs — pair with
469
+ `flags: [:mixed_content]`), `:callback` (raw value + document byte
470
+ offset + `type_tag` echo, matching `Node#byte_offset`). Namespace
471
+ binding lives on plans: `ns:` is `:none` (default), `:any`, or
472
+ `{ exact: "urn:..." }`; `flags:` also accepts `:cdata`, `:ordered`,
473
+ `:ns_lenient` (#754 out-of-namespace adoption). Children the plan
474
+ does not describe are skipped; undescribed document order within a
475
+ row is preserved. `#walk` returns a lazy `PlanValue` tree (the
476
+ result outlives the document); `#to_ruby` materializes it.
477
+
439
478
  == XQuery
440
479
 
441
480
  XQuery 1.0 core through `Leptris::XML::XQuery` (compile once,
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.160"
11
+ LIBLEPTRIS_VERSION = "1.9.162"
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.160.0"
4
+ VERSION = "1.9.162.0"
5
5
  end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ # Tree-shaped schema-descriptor materialization (libleptris 1.9.162,
6
+ # upstream #1039): compile a plan tree once, then materialize a whole
7
+ # subtree against it in ONE native pass — no per-element Ruby calls
8
+ # (#185's class of cost). Children the plan does not describe are
9
+ # skipped; matches within a row keep document order.
10
+ #
11
+ # descriptor = Leptris::XML::Descriptor.build(
12
+ # name: "catalog",
13
+ # attributes: [{ name: "version", kind: :scalar }],
14
+ # children: [
15
+ # { name: "item", kind: :nested, plan: {
16
+ # name: "item",
17
+ # attributes: [{ name: "id", kind: :scalar }],
18
+ # children: [
19
+ # { name: "name", kind: :scalar },
20
+ # { name: "price", kind: :scalar },
21
+ # ] } },
22
+ # ])
23
+ # descriptor.walk(doc.root).to_ruby
24
+ #
25
+ # Plan rows: :scalar, :collection (repeated matches), :nested (recurse
26
+ # via +plan:+), :raw (serialized subtree), :content (mixed-content
27
+ # text runs), :callback (raw value + byte position + type_tag echo).
28
+ # Namespace binding per plan: +ns:+ is :none (default), :any, or
29
+ # { exact: "urn:..." }; +flags: [:mixed_content, :ordered, :cdata,
30
+ # :ns_lenient].
31
+ class Leptris::XML::Descriptor
32
+ class Handle < ::FFI::AutoPointer
33
+ def self.release(ptr)
34
+ Leptris::XML::FFI.leptris_plan_free(ptr)
35
+ end
36
+ end
37
+
38
+ class ResultHandle < ::FFI::AutoPointer
39
+ def self.release(ptr)
40
+ Leptris::XML::FFI.leptris_plan_result_free(ptr)
41
+ end
42
+ end
43
+
44
+ KINDS = {
45
+ scalar: Leptris::XML::FFI::PLAN_KIND_SCALAR,
46
+ collection: Leptris::XML::FFI::PLAN_KIND_COLLECTION,
47
+ nested: Leptris::XML::FFI::PLAN_KIND_NESTED,
48
+ raw: Leptris::XML::FFI::PLAN_KIND_RAW,
49
+ content: Leptris::XML::FFI::PLAN_KIND_CONTENT,
50
+ callback: Leptris::XML::FFI::PLAN_KIND_CALLBACK,
51
+ }.freeze
52
+ private_constant :KINDS
53
+
54
+ FLAGS = {
55
+ mixed_content: Leptris::XML::FFI::PLAN_FLAG_MIXED_CONTENT,
56
+ ordered: Leptris::XML::FFI::PLAN_FLAG_ORDERED,
57
+ cdata: Leptris::XML::FFI::PLAN_FLAG_CDATA,
58
+ ns_lenient: Leptris::XML::FFI::PLAN_FLAG_NS_LENIENT,
59
+ }.freeze
60
+ private_constant :FLAGS
61
+
62
+ attr_reader :root
63
+
64
+ # +tree+ is the root plan (the hash shape documented on the
65
+ # class). Returns the compiled, engine-owned descriptor; the
66
+ # Ruby-side spec memory is transient (the engine deep-copies).
67
+ def self.build(tree)
68
+ abi = Leptris::XML::FFI.leptris_plan_abi_version
69
+ if abi != Leptris::XML::FFI::PLAN_ABI_VERSION
70
+ raise Leptris::XML::Error,
71
+ "descriptor ABI v#{abi} != binding v#{Leptris::XML::FFI::PLAN_ABI_VERSION}"
72
+ end
73
+ plans = []
74
+ root_index = flatten_plan(tree, plans)
75
+ spec, plans_pointer, anchors = pack_plans(plans)
76
+ spec[:abi_version] = abi
77
+ spec[:plan_count] = plans.size
78
+ spec[:plans] = plans_pointer
79
+ status = ::FFI::MemoryPointer.new(:int)
80
+ raw = Leptris::XML::FFI.leptris_plan_build(spec.pointer, status)
81
+ if raw.null?
82
+ raise Leptris::XML::Error,
83
+ "plan build failed (status=#{status.read_int})"
84
+ end
85
+ new(Handle.new(raw), plans, root_index)
86
+ end
87
+
88
+ # Flattens the nested hash tree into the engine's flat plans
89
+ # array (child rows reference plans by index). Returns the root
90
+ # plan's index — always 0 for a single root.
91
+ def self.flatten_plan(tree, plans)
92
+ tree = tree.dup
93
+ plans << tree
94
+ tree_index = plans.size - 1
95
+ tree[:children] = (tree[:children] || []).map do |row|
96
+ if row[:kind] == :nested
97
+ row.merge(child_plan_index: flatten_plan(row.fetch(:plan), plans))
98
+ else
99
+ row
100
+ end
101
+ end
102
+ tree_index
103
+ end
104
+ private_class_method :flatten_plan
105
+
106
+ # Packs the flattened plans into FFI memory: one ElementPlan per
107
+ # plan plus one AttrPlan/ChildPlan row per attribute/child. Every
108
+ # C string and row array is anchored in Ruby until the build call
109
+ # returns (the engine deep-copies).
110
+ def self.pack_plans(plans)
111
+ anchors = [nil]
112
+ plan_memory = ::FFI::MemoryPointer.new(
113
+ Leptris::XML::FFI::ElementPlan, plans.size)
114
+ anchors[0] = plan_memory
115
+ plans.each_with_index do |plan, i|
116
+ ep = Leptris::XML::FFI::ElementPlan.new(plan_memory[i])
117
+ ep[:element_name] = anchor_string(anchors, plan.fetch(:name))
118
+ ns = plan[:ns]
119
+ ns_form, ns_uri =
120
+ case ns
121
+ when nil, :none then [Leptris::XML::FFI::PLAN_NS_NONE, nil]
122
+ when :any then [Leptris::XML::FFI::PLAN_NS_ANY, nil]
123
+ when Hash
124
+ [Leptris::XML::FFI::PLAN_NS_EXACT,
125
+ anchor_string(anchors, ns.fetch(:exact))]
126
+ else
127
+ raise ArgumentError, "ns must be :none, :any, or {exact: uri}"
128
+ end
129
+ ep[:ns_form] = ns_form
130
+ ep[:ns_uri] = ns_uri
131
+ attrs = plan[:attributes] || []
132
+ attr_memory = attrs.empty? ? nil :
133
+ ::FFI::MemoryPointer.new(Leptris::XML::FFI::AttrPlan, attrs.size)
134
+ anchors << attr_memory
135
+ attrs.each_with_index do |row, j|
136
+ ap = Leptris::XML::FFI::AttrPlan.new(attr_memory[j])
137
+ ap[:wire_name] = anchor_string(anchors, row.fetch(:name))
138
+ ap[:kind] = kind_code(row[:kind] || :scalar)
139
+ ap[:type_tag] = row[:type_tag] || 0
140
+ end
141
+ ep[:attribute_count] = attrs.size
142
+ ep[:attribute_plans] = attr_memory
143
+ children = plan[:children] || []
144
+ child_memory = children.empty? ? nil :
145
+ ::FFI::MemoryPointer.new(Leptris::XML::FFI::ChildPlan, children.size)
146
+ anchors << child_memory
147
+ children.each_with_index do |row, j|
148
+ cp = Leptris::XML::FFI::ChildPlan.new(child_memory[j])
149
+ cp[:wire_name] = anchor_string(anchors, row.fetch(:name))
150
+ cp[:kind] = kind_code(row[:kind] || :scalar)
151
+ cp[:type_tag] = row[:type_tag] || 0
152
+ cp[:child_plan_index] = row[:child_plan_index] || -1
153
+ end
154
+ ep[:child_count] = children.size
155
+ ep[:child_plans] = child_memory
156
+ ep[:flags] = (plan[:flags] || []).map { |f| FLAGS.fetch(f) }.sum
157
+ end
158
+ [Leptris::XML::FFI::PlanSpec.new, plan_memory, anchors]
159
+ end
160
+ private_class_method :pack_plans
161
+
162
+ # Anchors the NUL-terminated copy in +anchors+ — the engine
163
+ # deep-copies at build, but the source bytes must live until the
164
+ # leptris_plan_build call returns.
165
+ def self.anchor_string(anchors, ruby_string)
166
+ ptr = ::FFI::MemoryPointer.from_string(ruby_string.to_s)
167
+ anchors << ptr
168
+ ptr
169
+ end
170
+ private_class_method :anchor_string
171
+
172
+ def self.kind_code(kind)
173
+ KINDS.fetch(kind) do
174
+ raise ArgumentError,
175
+ "kind must be one of #{KINDS.keys.inspect}, got #{kind.inspect}"
176
+ end
177
+ end
178
+ private_class_method :kind_code
179
+
180
+ def initialize(handle, plans, root_index)
181
+ @handle = handle
182
+ @plans = plans
183
+ @root_index = root_index
184
+ end
185
+
186
+ # Materializes the subtree rooted at +element_or_document+ (the
187
+ # root plan applies to the context node itself) in one native
188
+ # pass. Returns the lazy PlanValue tree — a standalone result
189
+ # that outlives the document.
190
+ def walk(element_or_document)
191
+ context = Leptris::XML::EvaluationContext.of(element_or_document)
192
+ status = ::FFI::MemoryPointer.new(:int)
193
+ raw = Leptris::XML::FFI.leptris_plan_walk(
194
+ context.document.c_ptr, context.context_node_ptr, @handle, status)
195
+ if raw.null?
196
+ raise Leptris::XML::Error,
197
+ "plan walk failed (status=#{status.read_int})"
198
+ end
199
+ Leptris::XML::PlanValue.new(ResultHandle.new(raw),
200
+ owner: true, plans: @plans, plan: @plans[0])
201
+ end
202
+ end
@@ -66,6 +66,8 @@ module Leptris
66
66
  typedef :pointer, :leptris_xquery
67
67
  typedef :pointer, :leptris_schematron
68
68
  typedef :pointer, :leptris_diff
69
+ typedef :pointer, :leptris_plan
70
+ typedef :pointer, :leptris_plan_result
69
71
  typedef :pointer, :leptris_sax_recorder
70
72
  typedef :pointer, :leptris_pull_parser
71
73
  typedef :pointer, :leptris_iterparse
@@ -153,6 +155,97 @@ module Leptris
153
155
  :text_len, :size_t
154
156
  end
155
157
 
158
+ # Tree-shaped schema-descriptor materialization (1.9.162,
159
+ # upstream #1039): hosts compile a plan tree once, then
160
+ # leptris_plan_walk materializes a whole subtree in one
161
+ # native pass. The plan/result structs mirror descriptor.h
162
+ # — POD, ABI-frozen at v1 (leptris_plan_abi_version).
163
+ class AttrPlan < ::FFI::Struct
164
+ layout :wire_name, :pointer,
165
+ :kind, :uint8,
166
+ :type_tag, :uint8
167
+ end
168
+ class ChildPlan < ::FFI::Struct
169
+ layout :wire_name, :pointer,
170
+ :kind, :uint8,
171
+ :type_tag, :uint8,
172
+ :child_plan_index, :int32
173
+ end
174
+ class ElementPlan < ::FFI::Struct
175
+ layout :element_name, :pointer,
176
+ :ns_form, :uint8,
177
+ :pad0, :uint8,
178
+ :ns_uri, :pointer,
179
+ :attribute_count, :uint32,
180
+ :child_count, :uint32,
181
+ :attribute_plans, :pointer,
182
+ :child_plans, :pointer,
183
+ :flags, :uint16,
184
+ :pad1, :uint16
185
+ end
186
+ class PlanSpec < ::FFI::Struct
187
+ layout :abi_version, :uint32,
188
+ :plan_count, :uint32,
189
+ :plans, :pointer
190
+ end
191
+
192
+ PLAN_ABI_VERSION = 1
193
+
194
+ PLAN_KIND_SCALAR = 1
195
+ PLAN_KIND_COLLECTION = 2
196
+ PLAN_KIND_NESTED = 3
197
+ PLAN_KIND_RAW = 4
198
+ PLAN_KIND_CONTENT = 5
199
+ PLAN_KIND_CALLBACK = 6
200
+
201
+ PLAN_FLAG_MIXED_CONTENT = 0x1
202
+ PLAN_FLAG_ORDERED = 0x2
203
+ PLAN_FLAG_CDATA = 0x4
204
+ PLAN_FLAG_NS_LENIENT = 0x8
205
+
206
+ PLAN_NS_NONE = 0
207
+ PLAN_NS_EXACT = 1
208
+ PLAN_NS_ANY = 2
209
+
210
+ PLAN_VALUE_ELEMENT = 0
211
+ PLAN_VALUE_SCALAR = 1
212
+ PLAN_VALUE_COLLECTION = 2
213
+ PLAN_VALUE_RAW = 3
214
+ PLAN_VALUE_CALLBACK = 4
215
+
216
+ # Byte offset of parse-created nodes (1.9.162, #1039): the
217
+ # position descriptor CALLBACK rows echo; 0 when unknown.
218
+ attach_function :leptris_node_byte_offset,
219
+ [:leptris_node_ref], :size_t
220
+
221
+ attach_function :leptris_plan_abi_version, [], :uint32
222
+ attach_function :leptris_plan_build,
223
+ [:pointer, :pointer], :leptris_plan
224
+ attach_function :leptris_plan_free, [:leptris_plan], :void
225
+ attach_function :leptris_plan_walk,
226
+ [:leptris_document, :leptris_element, :leptris_plan, :pointer],
227
+ :leptris_plan_result
228
+ attach_function :leptris_plan_result_free,
229
+ [:leptris_plan_result], :void
230
+ attach_function :leptris_plan_value_kind,
231
+ [:leptris_plan_result], :int
232
+ attach_function :leptris_plan_value_name,
233
+ [:leptris_plan_result], :string
234
+ attach_function :leptris_plan_value_type_tag,
235
+ [:leptris_plan_result], :uint8
236
+ attach_function :leptris_plan_value_string,
237
+ [:leptris_plan_result], :string
238
+ attach_function :leptris_plan_value_length,
239
+ [:leptris_plan_result], :size_t
240
+ attach_function :leptris_plan_value_position,
241
+ [:leptris_plan_result], :size_t
242
+ attach_function :leptris_plan_value_count,
243
+ [:leptris_plan_result], :size_t
244
+ attach_function :leptris_plan_value_at,
245
+ [:leptris_plan_result, :size_t], :leptris_plan_result
246
+ attach_function :leptris_plan_value_attribute,
247
+ [:leptris_plan_result, :string], :string
248
+
156
249
  attach_function :leptris_version, [], :string
157
250
  attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
158
251
 
@@ -199,6 +199,15 @@ class Leptris::XML::Node
199
199
  Leptris::XML::FFI.leptris_node_line(@c_ptr)
200
200
  end
201
201
 
202
+ # Byte offset of the node's markup in its parse source (the '<'
203
+ # of the tag; libleptris 1.9.162, #1039 — the position descriptor
204
+ # CALLBACK rows echo). 0 when unknown: mutation-created nodes, or
205
+ # documents >= 2 GiB.
206
+ def byte_offset
207
+ ensure_alive!
208
+ Leptris::XML::FFI.leptris_node_byte_offset(@c_ptr)
209
+ end
210
+
202
211
  def <=>(other)
203
212
  return nil unless other.is_a?(Leptris::XML::Node)
204
213
  return nil unless @document == other.document
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ # One node of a Descriptor#walk result (libleptris 1.9.162, upstream
6
+ # #1039): a lazy view over the engine-owned value tree. The ROOT
7
+ # value owns the whole-result handle (freed by GC); child values
8
+ # borrow from their parent — #at returns borrowed views, so the
9
+ # root must outlive them (hold the root, read freely).
10
+ #
11
+ # The C result API reads attributes BY NAME only, so element
12
+ # attribute enumeration threads the producing plan's attribute
13
+ # rows: Descriptor#walk plants the root plan, #to_ruby walks
14
+ # (value, plan) pairs in parallel.
15
+ class Leptris::XML::PlanValue
16
+ VALUE_KINDS = {
17
+ Leptris::XML::FFI::PLAN_VALUE_ELEMENT => :element,
18
+ Leptris::XML::FFI::PLAN_VALUE_SCALAR => :scalar,
19
+ Leptris::XML::FFI::PLAN_VALUE_COLLECTION => :collection,
20
+ Leptris::XML::FFI::PLAN_VALUE_RAW => :raw,
21
+ Leptris::XML::FFI::PLAN_VALUE_CALLBACK => :callback,
22
+ }.freeze
23
+ private_constant :VALUE_KINDS
24
+
25
+ def initialize(ptr, owner: nil, plans: nil, plan: nil)
26
+ @ptr = ptr
27
+ @owner = owner
28
+ @plans = plans
29
+ @plan = plan
30
+ end
31
+
32
+ def kind
33
+ VALUE_KINDS.fetch(Leptris::XML::FFI.leptris_plan_value_kind(@ptr))
34
+ end
35
+
36
+ # wire_name of the plan row that produced this value.
37
+ def name
38
+ Leptris::XML::FFI.leptris_plan_value_name(@ptr)
39
+ end
40
+
41
+ # Host type_tag echoed verbatim (0 when the row had none).
42
+ def type_tag
43
+ Leptris::XML::FFI.leptris_plan_value_type_tag(@ptr)
44
+ end
45
+
46
+ # SCALAR / RAW / CALLBACK string value.
47
+ def string_value
48
+ Leptris::XML::FFI.leptris_plan_value_string(@ptr)
49
+ end
50
+
51
+ # CALLBACK: document byte offset of the source node (0 unknown).
52
+ def position
53
+ Leptris::XML::FFI.leptris_plan_value_position(@ptr)
54
+ end
55
+
56
+ # ELEMENT: child-value count. COLLECTION: item count.
57
+ def count
58
+ Leptris::XML::FFI.leptris_plan_value_count(@ptr)
59
+ end
60
+
61
+ # ELEMENT child value / COLLECTION item at +i+ (nil out of
62
+ # range). Borrowed: the owning root must stay alive.
63
+ def at(i)
64
+ ptr = Leptris::XML::FFI.leptris_plan_value_at(@ptr, i)
65
+ return nil if ptr.null?
66
+ child_name = Leptris::XML::FFI.leptris_plan_value_name(ptr)
67
+ child_plan = @plan ? child_row_plan(child_name) : nil
68
+ Leptris::XML::PlanValue.new(ptr, plans: @plans, plan: child_plan)
69
+ end
70
+
71
+ # ELEMENT: attribute value by wire_name (nil when absent).
72
+ def attribute(wire_name)
73
+ Leptris::XML::FFI.leptris_plan_value_attribute(@ptr, wire_name.to_s)
74
+ end
75
+
76
+ # The eager Ruby tree:
77
+ # :element => { kind:, name:, type_tag:, attributes: {...},
78
+ # children: [values] }
79
+ # :scalar/:raw => String
80
+ # :collection => [values]
81
+ # :callback => { value:, position:, type_tag: }
82
+ def to_ruby
83
+ case kind
84
+ when :element
85
+ {
86
+ kind: :element,
87
+ name: name,
88
+ type_tag: type_tag,
89
+ attributes: collected_attributes,
90
+ children: Array.new(count) { |i| at(i).to_ruby },
91
+ }
92
+ when :collection
93
+ Array.new(count) { |i| at(i).to_ruby }
94
+ when :callback
95
+ { value: string_value, position: position, type_tag: type_tag }
96
+ else
97
+ string_value
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # The element plan that a child row with this wire_name recurses
104
+ # into (nil for non-nested rows — their values carry no plan).
105
+ def child_row_plan(child_wire_name)
106
+ row = (@plan[:children] || []).find do |r|
107
+ r[:kind] == :nested && r[:name] == child_wire_name
108
+ end
109
+ return nil unless row && @plans
110
+ @plans[row[:child_plan_index]]
111
+ end
112
+
113
+ def collected_attributes
114
+ result = {}
115
+ ((@plan && @plan[:attributes]) || []).each do |row|
116
+ v = attribute(row[:name])
117
+ result[row[:name]] = v unless v.nil?
118
+ end
119
+ result
120
+ end
121
+ end
data/lib/leptris/xml.rb CHANGED
@@ -25,6 +25,8 @@ module Leptris
25
25
  autoload :EvaluationContext, "leptris/xml/evaluation_context"
26
26
  autoload :Schematron, "leptris/xml/schematron"
27
27
  autoload :Diff, "leptris/xml/diff"
28
+ autoload :Descriptor, "leptris/xml/descriptor"
29
+ autoload :PlanValue, "leptris/xml/plan_value"
28
30
  autoload :IterationScope, "leptris/xml/iteration_scope"
29
31
  autoload :ResultText, "leptris/xml/result_text"
30
32
  autoload :ResultAttr, "leptris/xml/result_attr"
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.160.0
4
+ version: 1.9.162.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-13 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -106,6 +106,7 @@ files:
106
106
  - lib/leptris/xml/cdata.rb
107
107
  - lib/leptris/xml/comment.rb
108
108
  - lib/leptris/xml/css_to_xpath.rb
109
+ - lib/leptris/xml/descriptor.rb
109
110
  - lib/leptris/xml/diff.rb
110
111
  - lib/leptris/xml/doc_type.rb
111
112
  - lib/leptris/xml/document.rb
@@ -119,6 +120,7 @@ files:
119
120
  - lib/leptris/xml/node.rb
120
121
  - lib/leptris/xml/node_set.rb
121
122
  - lib/leptris/xml/parse_options.rb
123
+ - lib/leptris/xml/plan_value.rb
122
124
  - lib/leptris/xml/processing_instruction.rb
123
125
  - lib/leptris/xml/pull.rb
124
126
  - lib/leptris/xml/relaxng.rb