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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/builder.rb +169 -0
- data/lib/leptris/xml/descriptor.rb +42 -1
- data/lib/leptris/xml/document.rb +14 -0
- data/lib/leptris/xml/element.rb +11 -0
- data/lib/leptris/xml/ffi.rb +28 -3
- data/lib/leptris/xml/plan_value.rb +7 -0
- data/lib/leptris/xml.rb +1 -0
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +45 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/bindings/rust/descriptor/src/lib.rs +14 -0
- data/vendor-src/libleptris/bindings/rust/tests/descriptor_abi.rs +5 -0
- data/vendor-src/libleptris/src/include/leptris/descriptor.h +45 -1
- data/vendor-src/libleptris/src/leptris/descriptor/plan.c +529 -23
- data/vendor-src/libleptris/src/leptris/dom/element_modify.c +50 -0
- data/vendor-src/libleptris/test/abi/test_descriptor.cpp +736 -1
- data/vendor-src/libleptris/test/abi/test_dom_mutation_bugs.cpp +67 -0
- data/vendor-src/libleptris/test/perf/test_perf.cpp +7 -3
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-111.xml +17 -17
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-111.xsl +45 -45
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-115.xsl +19 -19
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-156.imp1.imp +16 -16
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-156.imp2.imp +10 -10
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-156.xsl +12 -12
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-165.xml +10 -10
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-80.xml +6 -6
- data/vendor-src/libleptris/test/xslt/libxslt_suite_general/bug-80.xsl +29 -29
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8be66f684edb1d18e35ab05b6926becf591022c5c776c8fb27c2bf7fb9c6578c
|
|
4
|
+
data.tar.gz: caf2d6f092fcc16dfe7931b8372fea55f244186a512612552f2bac89435e320c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
data/lib/leptris/version.rb
CHANGED
|
@@ -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
|
+
"&" => "&",
|
|
25
|
+
"<" => "<",
|
|
26
|
+
">" => ">",
|
|
27
|
+
'"' => """,
|
|
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
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -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
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -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
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -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,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
|
|
@@ -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 {
|