leptris 1.9.197.2-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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3890 -0
  3. data/CLAUDE.md +115 -0
  4. data/CONTEXT.md +39 -0
  5. data/LICENSE.md +33 -0
  6. data/README.adoc +755 -0
  7. data/Rakefile +407 -0
  8. data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
  9. data/TODO.perf/02-auto-enable-native-layer.md +19 -0
  10. data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
  11. data/TODO.perf/04-ext-bound-serialization.md +15 -0
  12. data/TODO.perf/05-native-surface-completion.md +19 -0
  13. data/TODO.perf/06-perf-battery-native-legs.md +16 -0
  14. data/TODO.perf/07-native-read-floor.md +31 -0
  15. data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
  16. data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
  17. data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
  18. data/TODO.perf/11-moxml-gap-battery.md +18 -0
  19. data/TODO.perf/12-document-lifetime-in-c.md +47 -0
  20. data/TODO.perf/13-structural-memos.md +49 -0
  21. data/TODO.perf/14-insert-family-c-bound.md +31 -0
  22. data/TODO.perf/15-compiled-expression-cache.md +35 -0
  23. data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
  24. data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
  25. data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
  26. data/TODO.perf/19-lazy-node-pointer.md +39 -0
  27. data/TODO.perf/20-css-translation-cache.md +24 -0
  28. data/TODO.perf/21-key-memo-consult.md +20 -0
  29. data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
  30. data/TODO.perf/23-cbound-value-mutations.md +27 -0
  31. data/TODO.perf/24-immutable-read-lanes.md +22 -0
  32. data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
  33. data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
  34. data/TODO.perf/27-c-yield-traversal.md +38 -0
  35. data/TODO.perf/28-address-fills.md +25 -0
  36. data/TODO.perf/29-ns-xpath-compiled.md +23 -0
  37. data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
  38. data/TODO.perf/31-battery-rounds-4-8.md +23 -0
  39. data/TODO.perf/32-clean-host-floor-table.md +29 -0
  40. data/TODO.perf/33-cbound-root-set.md +29 -0
  41. data/TODO.perf/34-fragment-fast-lane.md +41 -0
  42. data/TODO.perf/35-parse-default-c-face.md +24 -0
  43. data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
  44. data/TODO.perf/37-document-lazy-pointer.md +28 -0
  45. data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
  46. data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
  47. data/TODO.restructure/02-deep-copy-seam.md +21 -0
  48. data/TODO.restructure/03-evaluation-context-seam.md +18 -0
  49. data/TODO.restructure/04-spec-mece-restructure.md +20 -0
  50. data/TODO.restructure/05-memory-documentation.md +13 -0
  51. data/TODO.restructure/06-architecture-map-refresh.md +12 -0
  52. data/TODO.restructure/07-external-gates.md +16 -0
  53. data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
  54. data/TODO.restructure/09-iteration-scope.md +29 -0
  55. data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
  56. data/TODO.restructure/11-construction-factory-diet.md +16 -0
  57. data/TODO.restructure/12-resultattr-merge.md +12 -0
  58. data/TODO.restructure/13-spec-mece-followup.md +9 -0
  59. data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
  60. data/TODO.restructure/15-readme-feature-documentation.md +10 -0
  61. data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
  62. data/TODO.restructure/17-perf-drift-record.md +12 -0
  63. data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
  64. data/TODO.restructure/19-gitignore-hygiene.md +10 -0
  65. data/TODO.restructure/20-utf8proc-enablement.md +34 -0
  66. data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
  67. data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
  68. data/docs/adr/0001-lockstep-mirror.md +17 -0
  69. data/docs/adr/0002-utf8-at-the-seam.md +19 -0
  70. data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
  71. data/docs/adr/0004-lifetime-guard.md +21 -0
  72. data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
  73. data/docs/adr/0006-ruby-variant-policy.md +25 -0
  74. data/ext/build_windows_native.rb +42 -0
  75. data/ext/leptris/native/extconf.rb +55 -0
  76. data/ext/leptris/native/native.c +2646 -0
  77. data/leptris.gemspec +43 -0
  78. data/lib/leptris/version.rb +5 -0
  79. data/lib/leptris/xml/attr.rb +76 -0
  80. data/lib/leptris/xml/c_string_array.rb +37 -0
  81. data/lib/leptris/xml/cdata.rb +27 -0
  82. data/lib/leptris/xml/comment.rb +27 -0
  83. data/lib/leptris/xml/css_to_xpath.rb +192 -0
  84. data/lib/leptris/xml/descriptor.rb +214 -0
  85. data/lib/leptris/xml/diff.rb +74 -0
  86. data/lib/leptris/xml/doc_type.rb +54 -0
  87. data/lib/leptris/xml/document.rb +716 -0
  88. data/lib/leptris/xml/document_fragment.rb +63 -0
  89. data/lib/leptris/xml/element.rb +771 -0
  90. data/lib/leptris/xml/entity_reference.rb +13 -0
  91. data/lib/leptris/xml/evaluation_context.rb +32 -0
  92. data/lib/leptris/xml/ffi.rb +1527 -0
  93. data/lib/leptris/xml/iteration_scope.rb +56 -0
  94. data/lib/leptris/xml/iterparse.rb +144 -0
  95. data/lib/leptris/xml/namespace.rb +43 -0
  96. data/lib/leptris/xml/native.so +0 -0
  97. data/lib/leptris/xml/native_layer.rb +92 -0
  98. data/lib/leptris/xml/node.rb +686 -0
  99. data/lib/leptris/xml/node_set.rb +212 -0
  100. data/lib/leptris/xml/parse_options.rb +97 -0
  101. data/lib/leptris/xml/plan_value.rb +121 -0
  102. data/lib/leptris/xml/processing_instruction.rb +90 -0
  103. data/lib/leptris/xml/pull.rb +212 -0
  104. data/lib/leptris/xml/relaxng.rb +126 -0
  105. data/lib/leptris/xml/result_attr.rb +39 -0
  106. data/lib/leptris/xml/result_text.rb +36 -0
  107. data/lib/leptris/xml/sax/document.rb +45 -0
  108. data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
  109. data/lib/leptris/xml/sax/parser.rb +326 -0
  110. data/lib/leptris/xml/sax/recorder.rb +374 -0
  111. data/lib/leptris/xml/sax.rb +14 -0
  112. data/lib/leptris/xml/schematron.rb +88 -0
  113. data/lib/leptris/xml/searchable.rb +361 -0
  114. data/lib/leptris/xml/serialization.rb +180 -0
  115. data/lib/leptris/xml/text.rb +32 -0
  116. data/lib/leptris/xml/xpath.rb +89 -0
  117. data/lib/leptris/xml/xquery.rb +77 -0
  118. data/lib/leptris/xml/xslt.rb +85 -0
  119. data/lib/leptris/xml.rb +108 -0
  120. data/lib/leptris.rb +53 -0
  121. data/lib/libleptris.so +0 -0
  122. data/lib/libutf8proc.so.3 +0 -0
  123. data/scripts/gem_smoke.rb +36 -0
  124. 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