leptris 1.9.199.0-arm-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 +7 -0
- data/CHANGELOG.md +3890 -0
- data/CLAUDE.md +115 -0
- data/CONTEXT.md +39 -0
- data/LICENSE.md +33 -0
- data/README.adoc +755 -0
- data/Rakefile +407 -0
- data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
- data/TODO.perf/02-auto-enable-native-layer.md +19 -0
- data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
- data/TODO.perf/04-ext-bound-serialization.md +15 -0
- data/TODO.perf/05-native-surface-completion.md +19 -0
- data/TODO.perf/06-perf-battery-native-legs.md +16 -0
- data/TODO.perf/07-native-read-floor.md +31 -0
- data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
- data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
- data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
- data/TODO.perf/11-moxml-gap-battery.md +18 -0
- data/TODO.perf/12-document-lifetime-in-c.md +47 -0
- data/TODO.perf/13-structural-memos.md +49 -0
- data/TODO.perf/14-insert-family-c-bound.md +31 -0
- data/TODO.perf/15-compiled-expression-cache.md +35 -0
- data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
- data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
- data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
- data/TODO.perf/19-lazy-node-pointer.md +39 -0
- data/TODO.perf/20-css-translation-cache.md +24 -0
- data/TODO.perf/21-key-memo-consult.md +20 -0
- data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
- data/TODO.perf/23-cbound-value-mutations.md +27 -0
- data/TODO.perf/24-immutable-read-lanes.md +22 -0
- data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
- data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
- data/TODO.perf/27-c-yield-traversal.md +38 -0
- data/TODO.perf/28-address-fills.md +25 -0
- data/TODO.perf/29-ns-xpath-compiled.md +23 -0
- data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
- data/TODO.perf/31-battery-rounds-4-8.md +23 -0
- data/TODO.perf/32-clean-host-floor-table.md +29 -0
- data/TODO.perf/33-cbound-root-set.md +29 -0
- data/TODO.perf/34-fragment-fast-lane.md +41 -0
- data/TODO.perf/35-parse-default-c-face.md +24 -0
- data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
- data/TODO.perf/37-document-lazy-pointer.md +28 -0
- data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
- data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
- data/TODO.restructure/02-deep-copy-seam.md +21 -0
- data/TODO.restructure/03-evaluation-context-seam.md +18 -0
- data/TODO.restructure/04-spec-mece-restructure.md +20 -0
- data/TODO.restructure/05-memory-documentation.md +13 -0
- data/TODO.restructure/06-architecture-map-refresh.md +12 -0
- data/TODO.restructure/07-external-gates.md +16 -0
- data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
- data/TODO.restructure/09-iteration-scope.md +29 -0
- data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
- data/TODO.restructure/11-construction-factory-diet.md +16 -0
- data/TODO.restructure/12-resultattr-merge.md +12 -0
- data/TODO.restructure/13-spec-mece-followup.md +9 -0
- data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
- data/TODO.restructure/15-readme-feature-documentation.md +10 -0
- data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
- data/TODO.restructure/17-perf-drift-record.md +12 -0
- data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
- data/TODO.restructure/19-gitignore-hygiene.md +10 -0
- data/TODO.restructure/20-utf8proc-enablement.md +34 -0
- data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
- data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
- data/docs/adr/0001-lockstep-mirror.md +17 -0
- data/docs/adr/0002-utf8-at-the-seam.md +19 -0
- data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
- data/docs/adr/0004-lifetime-guard.md +21 -0
- data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
- data/docs/adr/0006-ruby-variant-policy.md +25 -0
- data/ext/build_windows_native.rb +42 -0
- data/ext/leptris/native/extconf.rb +55 -0
- data/ext/leptris/native/native.c +2528 -0
- data/leptris.gemspec +43 -0
- data/lib/leptris/version.rb +5 -0
- data/lib/leptris/xml/attr.rb +76 -0
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +27 -0
- data/lib/leptris/xml/comment.rb +27 -0
- data/lib/leptris/xml/css_to_xpath.rb +192 -0
- data/lib/leptris/xml/descriptor.rb +214 -0
- data/lib/leptris/xml/diff.rb +74 -0
- data/lib/leptris/xml/doc_type.rb +54 -0
- data/lib/leptris/xml/document.rb +716 -0
- data/lib/leptris/xml/document_fragment.rb +63 -0
- data/lib/leptris/xml/element.rb +771 -0
- data/lib/leptris/xml/entity_reference.rb +13 -0
- data/lib/leptris/xml/evaluation_context.rb +32 -0
- data/lib/leptris/xml/ffi.rb +1527 -0
- data/lib/leptris/xml/iteration_scope.rb +56 -0
- data/lib/leptris/xml/iterparse.rb +144 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/native.so +0 -0
- data/lib/leptris/xml/native_layer.rb +92 -0
- data/lib/leptris/xml/node.rb +686 -0
- data/lib/leptris/xml/node_set.rb +212 -0
- data/lib/leptris/xml/parse_options.rb +97 -0
- data/lib/leptris/xml/plan_value.rb +121 -0
- data/lib/leptris/xml/processing_instruction.rb +90 -0
- data/lib/leptris/xml/pull.rb +212 -0
- data/lib/leptris/xml/relaxng.rb +126 -0
- data/lib/leptris/xml/result_attr.rb +39 -0
- data/lib/leptris/xml/result_text.rb +36 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
- data/lib/leptris/xml/sax/parser.rb +326 -0
- data/lib/leptris/xml/sax/recorder.rb +374 -0
- data/lib/leptris/xml/sax.rb +14 -0
- data/lib/leptris/xml/schematron.rb +88 -0
- data/lib/leptris/xml/searchable.rb +361 -0
- data/lib/leptris/xml/serialization.rb +180 -0
- data/lib/leptris/xml/text.rb +32 -0
- data/lib/leptris/xml/xpath.rb +89 -0
- data/lib/leptris/xml/xquery.rb +77 -0
- data/lib/leptris/xml/xslt.rb +85 -0
- data/lib/leptris/xml.rb +108 -0
- data/lib/leptris.rb +53 -0
- data/lib/libleptris.so +0 -0
- data/lib/libutf8proc.so.3 +0 -0
- data/scripts/gem_smoke.rb +36 -0
- metadata +214 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
class Leptris::XML::NodeSet
|
|
6
|
+
include Enumerable
|
|
7
|
+
include Leptris::XML::Searchable
|
|
8
|
+
|
|
9
|
+
attr_reader :document
|
|
10
|
+
|
|
11
|
+
# Two construction modes:
|
|
12
|
+
# - eager: pass an Array of Nodes (e.g. Element#children builds one)
|
|
13
|
+
# - lazy: pass an FFI::Pointer to a LeptrisXPathResult that this NodeSet
|
|
14
|
+
# will keep alive and free on GC. Each [i] / each call goes
|
|
15
|
+
# through leptris_xpath_result_get instead of pre-materializing.
|
|
16
|
+
def initialize(document, source = nil)
|
|
17
|
+
@document = document
|
|
18
|
+
case source
|
|
19
|
+
when ::FFI::Pointer
|
|
20
|
+
if source.null?
|
|
21
|
+
@result_ptr = nil
|
|
22
|
+
@array = []
|
|
23
|
+
else
|
|
24
|
+
# Wrap in AutoPointer for automatic GC-time cleanup. NodeSet has no
|
|
25
|
+
# explicit #free method (Nokogiri doesn't either), so the AutoPointer
|
|
26
|
+
# double-free risk that Document#free hit doesn't apply here.
|
|
27
|
+
@result_ptr = ::FFI::AutoPointer.new(source, Leptris::XML::FFI.method(:leptris_xpath_result_free))
|
|
28
|
+
@array = nil
|
|
29
|
+
end
|
|
30
|
+
when nil
|
|
31
|
+
@result_ptr = nil
|
|
32
|
+
@array = []
|
|
33
|
+
else
|
|
34
|
+
@result_ptr = nil
|
|
35
|
+
@array = source.to_a
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.from_result(document, result_ptr)
|
|
40
|
+
new(document, result_ptr)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def length
|
|
44
|
+
return @array.length if @array
|
|
45
|
+
@result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : 0
|
|
46
|
+
end
|
|
47
|
+
alias_method :size, :length
|
|
48
|
+
|
|
49
|
+
def empty?
|
|
50
|
+
length == 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The materialized array (after to_a) is authoritative: every
|
|
54
|
+
# reader consults it first so a materialized set stops paying the
|
|
55
|
+
# batch fetch on each iteration or index.
|
|
56
|
+
def [](idx)
|
|
57
|
+
return @array[idx] if @array
|
|
58
|
+
# Negative indexes are Ruby-Array semantics (Nokogiri's NodeSet
|
|
59
|
+
# is slice-like); materialize rather than answer nil
|
|
60
|
+
# inconsistently with an eager set.
|
|
61
|
+
return to_a[idx] if idx.negative?
|
|
62
|
+
if @result_ptr
|
|
63
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
|
|
64
|
+
return nil if ptr.null?
|
|
65
|
+
Leptris::XML::Node.wrap(
|
|
66
|
+
ptr, @document,
|
|
67
|
+
result_value: text_item_value(
|
|
68
|
+
Leptris::XML::FFI.leptris_xpath_result_node_kind(@result_ptr, idx), idx))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Sequence/map/array items ride synthetic text nodes whose value
|
|
73
|
+
# only the live result handle can serve — capture it now.
|
|
74
|
+
def native_fast?
|
|
75
|
+
defined?(Leptris::XML::NATIVE_FAST)
|
|
76
|
+
end
|
|
77
|
+
private :native_fast?
|
|
78
|
+
|
|
79
|
+
def text_item_value(kind, index)
|
|
80
|
+
case kind
|
|
81
|
+
when Leptris::XML::FFI::XPATH_NODE_TEXT
|
|
82
|
+
Leptris::XML::FFI.leptris_xpath_result_node_value(@result_ptr, index)
|
|
83
|
+
when Leptris::XML::FFI::XPATH_NODE_ATTRIBUTE
|
|
84
|
+
# ruby#153: capture both faces — only the live result handle
|
|
85
|
+
# can serve an attribute node's name and value.
|
|
86
|
+
{
|
|
87
|
+
name: Leptris::XML::FFI.leptris_xpath_result_node_name(@result_ptr, index),
|
|
88
|
+
value: Leptris::XML::FFI.leptris_xpath_result_node_value(@result_ptr, index)
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
private :text_item_value
|
|
93
|
+
|
|
94
|
+
# Iteration materializes: the first pass batch-fetches into @array
|
|
95
|
+
# (yielding as it wraps), and every subsequent reader — each, [],
|
|
96
|
+
# length — serves from the array. Repeated iteration without an
|
|
97
|
+
# explicit to_a pays the batch exactly once.
|
|
98
|
+
def each
|
|
99
|
+
return enum_for(:each) unless block_given?
|
|
100
|
+
unless @array
|
|
101
|
+
return self unless @result_ptr
|
|
102
|
+
n = length
|
|
103
|
+
if n > 0 && native_fast?
|
|
104
|
+
# TODO.perf/03: one C pass materializes the whole result —
|
|
105
|
+
# class dispatch, synthetic text/attr value capture, and
|
|
106
|
+
# element identity-cache check/store in the ext. Qundef
|
|
107
|
+
# slots (rare non-element/attr/text kinds) and the >512
|
|
108
|
+
# remainder fall back to the per-index path below.
|
|
109
|
+
fast = Leptris::XML::Native.bulk_xpath(
|
|
110
|
+
@document, @result_ptr.to_i)
|
|
111
|
+
nodes = []
|
|
112
|
+
fast.each_with_index do |node, i|
|
|
113
|
+
if node.equal?(Leptris::XML::Native)
|
|
114
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
115
|
+
next if ptr.null?
|
|
116
|
+
nodes << Leptris::XML::Node.wrap(
|
|
117
|
+
ptr, @document,
|
|
118
|
+
result_value: text_item_value(
|
|
119
|
+
Leptris::XML::FFI.leptris_xpath_result_node_kind(@result_ptr, i), i))
|
|
120
|
+
else
|
|
121
|
+
nodes << node
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
(fast.length...n).each do |i|
|
|
125
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
126
|
+
next if ptr.null?
|
|
127
|
+
nodes << Leptris::XML::Node.wrap(
|
|
128
|
+
ptr, @document,
|
|
129
|
+
result_value: text_item_value(
|
|
130
|
+
Leptris::XML::FFI.leptris_xpath_result_node_kind(@result_ptr, i), i))
|
|
131
|
+
end
|
|
132
|
+
@array = nodes
|
|
133
|
+
elsif n > 0
|
|
134
|
+
# Batch-fetch all node pointers through the FFI seam
|
|
135
|
+
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
136
|
+
# not just elements); the batch accessor under-copies
|
|
137
|
+
# mixed-kind results (upstream leptris#477), so per-index
|
|
138
|
+
# fetch covers the remainder.
|
|
139
|
+
pointers, kinds, raw = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
|
|
140
|
+
nodes = []
|
|
141
|
+
pointers.each_with_index do |ptr, i|
|
|
142
|
+
next if ptr.null?
|
|
143
|
+
nodes << Leptris::XML::Node.wrap(
|
|
144
|
+
ptr, @document, node_type: kinds[i],
|
|
145
|
+
result_value: text_item_value(raw[i], i))
|
|
146
|
+
end
|
|
147
|
+
(pointers.length...n).each do |i|
|
|
148
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
149
|
+
next if ptr.null?
|
|
150
|
+
nodes << Leptris::XML::Node.wrap(
|
|
151
|
+
ptr, @document,
|
|
152
|
+
result_value: text_item_value(
|
|
153
|
+
Leptris::XML::FFI.leptris_xpath_result_node_kind(@result_ptr, i), i))
|
|
154
|
+
end
|
|
155
|
+
@array = nodes
|
|
156
|
+
else
|
|
157
|
+
@array = []
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
@array.each { |n| yield n }
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_a
|
|
165
|
+
return @array if @array
|
|
166
|
+
return [] if @result_ptr.nil?
|
|
167
|
+
each { |n| } # materializes @array as a side effect
|
|
168
|
+
@array
|
|
169
|
+
end
|
|
170
|
+
alias_method :to_ary, :to_a
|
|
171
|
+
|
|
172
|
+
def first(n = nil)
|
|
173
|
+
return self[0] if n.nil?
|
|
174
|
+
n.times.map { |i| self[i] }.take_while { |x| !x.nil? }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def last
|
|
178
|
+
if @result_ptr
|
|
179
|
+
self[length - 1]
|
|
180
|
+
else
|
|
181
|
+
@array.last
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def inner_text
|
|
186
|
+
map(&:content).join
|
|
187
|
+
end
|
|
188
|
+
alias_method :text, :inner_text
|
|
189
|
+
|
|
190
|
+
# Union semantics: ONE engine call evaluates the expression
|
|
191
|
+
# against every element member and merges the results into a
|
|
192
|
+
# de-duplicated, document-ordered nodeset (libleptris 1.9.7,
|
|
193
|
+
# upstream #589 — the per-member Ruby loop this replaces could
|
|
194
|
+
# neither dedup nor order).
|
|
195
|
+
def xpath(*paths)
|
|
196
|
+
handler, _ns, _vars = parse_search_args(paths)
|
|
197
|
+
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
198
|
+
expr = paths.join(" | ")
|
|
199
|
+
contexts = to_a.select { |node| node.is_a?(Leptris::XML::Element) }
|
|
200
|
+
return Leptris::XML::NodeSet.new(@document, []) if contexts.empty?
|
|
201
|
+
buffer = Leptris::XML::FFI.scratch_pointers(contexts.size)
|
|
202
|
+
buffer.write_array_of_pointer(contexts.map(&:c_ptr))
|
|
203
|
+
result_ptr = Leptris::XML::FFI.leptris_xpath_eval_nodeset(
|
|
204
|
+
@document.c_ptr, buffer, contexts.size, expr)
|
|
205
|
+
return Leptris::XML::NodeSet.new(@document, []) if result_ptr.null?
|
|
206
|
+
Leptris::XML::NodeSet.from_result(@document, result_ptr)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def inspect
|
|
210
|
+
to_a.inspect
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parser flags, mirroring LeptrisParseFlags in libleptris's public
|
|
4
|
+
# headers. Pass an instance as `Leptris::XML.parse(xml, options:)`.
|
|
5
|
+
class Leptris::XML::ParseOptions
|
|
6
|
+
# Discard whitespace-only text nodes (runs between tags that contain
|
|
7
|
+
# nothing but spaces/tabs/newlines). Matches libxml2's
|
|
8
|
+
# XML_PARSE_NOBLANKS and Nokogiri's noblanks. Documents parsed this
|
|
9
|
+
# way do not round-trip pretty-printed formatting byte-for-byte.
|
|
10
|
+
NOBLANKS = Leptris::XML::FFI::LEPTRIS_PARSE_DROP_WS_TEXT
|
|
11
|
+
|
|
12
|
+
# Apply DTD ATTLIST default (and #FIXED) attribute values at parse
|
|
13
|
+
# time (libleptris >= 1.9.8, leptris/leptris#606). Matches libxml2's
|
|
14
|
+
# XML_PARSE_DTDATTR and Nokogiri's dtdload+dtdattr semantics. Off by
|
|
15
|
+
# default: the ecosystem compares against libxml2's no-DTDATTR
|
|
16
|
+
# default, and W3C C14N 1.1 example 3.3's canonical form excludes
|
|
17
|
+
# defaulted attributes.
|
|
18
|
+
DTDATTR = Leptris::XML::FFI::LEPTRIS_PARSE_DTDATTR
|
|
19
|
+
|
|
20
|
+
# Keep &name; entity references unexpanded as first-class
|
|
21
|
+
# EntityReference nodes (libleptris 1.9.176, upstream #1094 /
|
|
22
|
+
# leptris-ruby#212): text runs split around them, character
|
|
23
|
+
# references still expand, and the references serialize back
|
|
24
|
+
# verbatim.
|
|
25
|
+
KEEP_ENTITY_REFS = Leptris::XML::FFI::LEPTRIS_PARSE_KEEP_ENTITY_REFS
|
|
26
|
+
|
|
27
|
+
attr_reader :flags
|
|
28
|
+
|
|
29
|
+
# Recover (libleptris 1.9.0, #547): a parse failure returns an
|
|
30
|
+
# empty document with the failure recorded in Document#last_error
|
|
31
|
+
# instead of raising — the libxml2 XML_PARSE_RECOVER semantics
|
|
32
|
+
# adapters emulate. Struct-only (not a parse flag): carrying it
|
|
33
|
+
# routes Document.parse through leptris_parse_string_ex.
|
|
34
|
+
attr_reader :recover
|
|
35
|
+
|
|
36
|
+
def initialize(flags = Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT,
|
|
37
|
+
recover: false)
|
|
38
|
+
@flags = flags.to_i
|
|
39
|
+
@recover = recover ? true : false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.keep_entity_refs
|
|
43
|
+
new(KEEP_ENTITY_REFS)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.noblanks
|
|
47
|
+
new(NOBLANKS)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.recovering
|
|
51
|
+
new(recover: true)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def noblanks?
|
|
55
|
+
@flags & NOBLANKS != 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.dtdattr
|
|
59
|
+
new(DTDATTR)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dtdattr?
|
|
63
|
+
@flags & DTDATTR != 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def keep_entity_refs?
|
|
67
|
+
@flags & KEEP_ENTITY_REFS != 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def dtdattr=(value)
|
|
71
|
+
if value then @flags |= DTDATTR else @flags &= ~DTDATTR end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def recover?
|
|
75
|
+
@recover == true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def |(other)
|
|
79
|
+
self.class.new(@flags | other.flags, recover: @recover || other.recover?)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# True when the options cannot ride the flags-only parse path and
|
|
83
|
+
# need the full LeptrisParseOptions struct (leptris_parse_string_ex).
|
|
84
|
+
def struct_required?
|
|
85
|
+
recover?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Builds the C LeptrisParseOptions struct mirroring this instance.
|
|
89
|
+
def to_c_struct
|
|
90
|
+
struct = Leptris::XML::FFI::ParseOptionsStruct.new
|
|
91
|
+
struct[:flags] = @flags
|
|
92
|
+
struct[:strict_mode] = -1
|
|
93
|
+
struct[:max_depth] = 0
|
|
94
|
+
struct[:recover] = @recover ? 1 : 0
|
|
95
|
+
struct
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
4
|
+
def name
|
|
5
|
+
return @name if memo_hit?(@name_version)
|
|
6
|
+
ensure_alive!
|
|
7
|
+
result = if @addr_reads_fast
|
|
8
|
+
Leptris::XML::Native.fast_pi_target(@c_address)
|
|
9
|
+
else
|
|
10
|
+
Leptris::XML::FFI.leptris_pi_node_get_target(c_ptr)
|
|
11
|
+
end
|
|
12
|
+
if @document
|
|
13
|
+
@name = result
|
|
14
|
+
@name_version = @document.version
|
|
15
|
+
end
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
alias_method :target, :name
|
|
19
|
+
|
|
20
|
+
def content
|
|
21
|
+
return @content if memo_hit?(@content_version)
|
|
22
|
+
ensure_alive!
|
|
23
|
+
raw = if @addr_reads_fast
|
|
24
|
+
Leptris::XML::Native.fast_pi_data(@c_address)
|
|
25
|
+
else
|
|
26
|
+
Leptris::XML::FFI.leptris_pi_node_get_data(c_ptr)
|
|
27
|
+
end
|
|
28
|
+
result = Leptris::XML::FFI.read_pi_data(raw).to_s
|
|
29
|
+
if @document
|
|
30
|
+
@content = result
|
|
31
|
+
@content_version = @document.version
|
|
32
|
+
end
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Since libleptris 1.9.9 (#612) parse-created document-level PIs
|
|
37
|
+
# carry document linkage — the setters work on them like on any
|
|
38
|
+
# tree-level PI.
|
|
39
|
+
def target=(new_target)
|
|
40
|
+
ensure_writable!
|
|
41
|
+
Leptris::XML::FFI.check_status(
|
|
42
|
+
Leptris::XML::FFI.leptris_pi_node_set_target(c_ptr, new_target.to_s))
|
|
43
|
+
new_target
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def data=(new_data)
|
|
47
|
+
ensure_writable!
|
|
48
|
+
Leptris::XML::FFI.check_status(
|
|
49
|
+
Leptris::XML::FFI.leptris_pi_node_set_data(c_ptr, new_data.to_s))
|
|
50
|
+
new_data
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Document-level PIs have no tree parent for leptris_node_unlink
|
|
54
|
+
# — route through the document-level removal (libleptris 1.9.9,
|
|
55
|
+
# #612), identity-matched by index so the right same-target PI
|
|
56
|
+
# comes out.
|
|
57
|
+
def unlink
|
|
58
|
+
ensure_writable!
|
|
59
|
+
rc = Leptris::XML::FFI.leptris_node_unlink(c_ptr)
|
|
60
|
+
if rc == Leptris::XML::FFI::LEPTRIS_ERROR_NOT_FOUND
|
|
61
|
+
index = document_pi_index
|
|
62
|
+
if index.negative?
|
|
63
|
+
raise Leptris::XML::Error,
|
|
64
|
+
"PI not found in its document — already removed?"
|
|
65
|
+
end
|
|
66
|
+
removed = @document.remove_pi(index)
|
|
67
|
+
unless removed && removed.c_ptr == c_ptr
|
|
68
|
+
raise Leptris::XML::Error, "document-level PI removal missed"
|
|
69
|
+
end
|
|
70
|
+
@parent = nil
|
|
71
|
+
return self
|
|
72
|
+
end
|
|
73
|
+
Leptris::XML::FFI.check_status(rc)
|
|
74
|
+
@parent = nil
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# This PI's 0-based index among the document's PIs (document
|
|
81
|
+
# order — the same enumeration leptris_document_remove_pi uses).
|
|
82
|
+
def document_pi_index
|
|
83
|
+
pi_index = -1
|
|
84
|
+
@document.children.each do |child|
|
|
85
|
+
pi_index += 1 if child.pi?
|
|
86
|
+
return pi_index if child.c_ptr == c_ptr
|
|
87
|
+
end
|
|
88
|
+
-1
|
|
89
|
+
end
|
|
90
|
+
end
|