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 +4 -4
- data/CHANGELOG.md +25 -1
- data/README.adoc +39 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/descriptor.rb +202 -0
- data/lib/leptris/xml/ffi.rb +93 -0
- data/lib/leptris/xml/node.rb +9 -0
- data/lib/leptris/xml/plan_value.rb +121 -0
- data/lib/leptris/xml.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c862a7a722a5595a09c5f2507c0f27cfb17a15d5a3216147301b07dcde63d75e
|
|
4
|
+
data.tar.gz: 2e672c08d5f6149c8bbda83fa4bb9be56ca981a290172a8e9fb4835c86d6ea58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
data/lib/leptris/version.rb
CHANGED
|
@@ -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
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -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
|
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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
|