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.
- 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 +2646 -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
|
+
# Pull parsing (libleptris v1.6.0): a StAX-style cursor over a
|
|
6
|
+
# document. Each #next advances one event; attributes are readable
|
|
7
|
+
# only during the start_element event just returned.
|
|
8
|
+
#
|
|
9
|
+
# Leptris::XML::Pull.parse(xml).each do |event|
|
|
10
|
+
# case event.type
|
|
11
|
+
# when :start_element then handle(event.name, event.attrs)
|
|
12
|
+
# when :text then handle(event.text)
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
module Leptris::XML::Pull
|
|
17
|
+
# Constructed positionally by the parser: the keyword-init
|
|
18
|
+
# variant cost ~220ns/event in batch drains (round XXI). Readers
|
|
19
|
+
# (event.type, event.attrs, ...) are unchanged.
|
|
20
|
+
Event = Struct.new(:type, :name, :text, :attrs)
|
|
21
|
+
|
|
22
|
+
TYPES = {
|
|
23
|
+
Leptris::XML::FFI::PULL_START_ELEMENT => :start_element,
|
|
24
|
+
Leptris::XML::FFI::PULL_END_ELEMENT => :end_element,
|
|
25
|
+
Leptris::XML::FFI::PULL_TEXT => :text,
|
|
26
|
+
Leptris::XML::FFI::PULL_COMMENT => :comment,
|
|
27
|
+
Leptris::XML::FFI::PULL_CDATA => :cdata,
|
|
28
|
+
Leptris::XML::FFI::PULL_PI => :pi,
|
|
29
|
+
Leptris::XML::FFI::PULL_END_DOCUMENT => :end_document,
|
|
30
|
+
Leptris::XML::FFI::PULL_ERROR => :error,
|
|
31
|
+
# Prefix events ride the stream ("" prefix = the default
|
|
32
|
+
# namespace): the engine has always delivered them; the map
|
|
33
|
+
# lacked the kinds, silently yielding nil-typed Events — which
|
|
34
|
+
# the staging guard then misread as corruption after the real
|
|
35
|
+
# engine fix landed (leptris/leptris#648's residual).
|
|
36
|
+
Leptris::XML::FFI::PULL_START_PREFIX => :start_prefix,
|
|
37
|
+
Leptris::XML::FFI::PULL_END_PREFIX => :end_prefix,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
class Parser
|
|
41
|
+
def self.parse(xml_or_io)
|
|
42
|
+
xml = xml_or_io.is_a?(String) ? xml_or_io : xml_or_io.read
|
|
43
|
+
new(Leptris::XML::FFI.leptris_pull_new(xml, xml.bytesize))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.parse_file(path)
|
|
47
|
+
new(Leptris::XML::FFI.leptris_pull_new_file(path))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(handle)
|
|
51
|
+
raise Leptris::XML::ParseError, "leptris_pull_new failed" if handle.null?
|
|
52
|
+
@handle = handle
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Releases the C parser. Safe to call twice.
|
|
56
|
+
def free
|
|
57
|
+
return if @handle.nil?
|
|
58
|
+
Leptris::XML::FFI.leptris_pull_free(@handle)
|
|
59
|
+
@handle = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def each
|
|
63
|
+
return enum_for(:each) unless block_given?
|
|
64
|
+
loop do
|
|
65
|
+
event = next_event
|
|
66
|
+
break if event.nil?
|
|
67
|
+
yield event
|
|
68
|
+
break if event.type == :end_document || event.type == :error
|
|
69
|
+
end
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Bulk-delivers events (libleptris 1.9.7, upstream #589): ONE C
|
|
74
|
+
# call stages up to +max_count+ events — the per-event dispatch
|
|
75
|
+
# that made streaming ~145x slower than a DOM parse collapses to
|
|
76
|
+
# amortized noise. Per the engine's protocol, attributes are
|
|
77
|
+
# captured only for the LAST start_element of each batch
|
|
78
|
+
# (earlier starts in the same batch carry nil attrs — segment
|
|
79
|
+
# boundaries fall where they fall); use #each when every
|
|
80
|
+
# start's attrs must be captured.
|
|
81
|
+
def each_batch(max_count = 256)
|
|
82
|
+
return enum_for(:each_batch, max_count) unless block_given?
|
|
83
|
+
stride = Leptris::XML::FFI::PullEventStruct.size
|
|
84
|
+
type_off = 0
|
|
85
|
+
name_off = Leptris::XML::FFI::PullEventStruct.offset_of(:name)
|
|
86
|
+
text_off = Leptris::XML::FFI::PullEventStruct.offset_of(:text)
|
|
87
|
+
buffer = Leptris::XML::FFI.scratch_events(max_count)
|
|
88
|
+
while (n = Leptris::XML::FFI.leptris_pull_next_batch(
|
|
89
|
+
@handle, buffer, max_count)) > 0
|
|
90
|
+
events = Array.new(n) do |i|
|
|
91
|
+
base = i * stride
|
|
92
|
+
name_ptr = buffer.get_pointer(base + name_off)
|
|
93
|
+
text_ptr = buffer.get_pointer(base + text_off)
|
|
94
|
+
type = TYPES[buffer.get_int(base + type_off)]
|
|
95
|
+
name = name_ptr.null? ? nil :
|
|
96
|
+
name_ptr.read_string.force_encoding(Encoding::UTF_8)
|
|
97
|
+
# Staging-corruption guard (leptris/leptris#648): the C
|
|
98
|
+
# batch's staging arena misplaces record strings once the
|
|
99
|
+
# staged content crosses its block boundary (~185 bytes of
|
|
100
|
+
# attribute value under nested attr-carrying ancestors) —
|
|
101
|
+
# names come back empty, control-byte-laced, or
|
|
102
|
+
# invalid-encoding, and unknown type codes surface as nil.
|
|
103
|
+
# Element names can never look like that: fail loudly
|
|
104
|
+
# instead of delivering garbage, and point at the cursor
|
|
105
|
+
# path, which reads the same documents correctly.
|
|
106
|
+
if type.nil? ||
|
|
107
|
+
(name && (!name.valid_encoding? ||
|
|
108
|
+
name.match?(/[\x00-\x08\x0e-\x1f]/))) ||
|
|
109
|
+
(%i[start_element end_element pi].include?(type) &&
|
|
110
|
+
(name.nil? || name.empty?))
|
|
111
|
+
raise Leptris::XML::Error,
|
|
112
|
+
"pull batch staging corruption (leptris/leptris#648) — " \
|
|
113
|
+
"use Parser#each (cursor) for this document"
|
|
114
|
+
end
|
|
115
|
+
text = text_ptr.null? ? nil :
|
|
116
|
+
text_ptr.read_string.force_encoding(Encoding::UTF_8)
|
|
117
|
+
text = Leptris::XML::FFI.read_pi_data(text) if type == :pi
|
|
118
|
+
Event.new(type, name, text, nil)
|
|
119
|
+
end
|
|
120
|
+
# The attr mirror holds the batch's most recent start.
|
|
121
|
+
last_start = nil
|
|
122
|
+
events.each_with_index do |event, i|
|
|
123
|
+
last_start = i if event.type == :start_element
|
|
124
|
+
end
|
|
125
|
+
events[last_start].attrs = capture_attrs if last_start
|
|
126
|
+
events.each { |event| yield event }
|
|
127
|
+
tail = events.last.type
|
|
128
|
+
break if tail == :end_document || tail == :error
|
|
129
|
+
end
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Hot-loop offsets: derived from PullEventStruct's layout (the
|
|
134
|
+
# struct stays the single source of truth for the ABI), read via
|
|
135
|
+
# get_int/get_pointer so the per-event struct-wrapper allocation
|
|
136
|
+
# disappears from streaming loops.
|
|
137
|
+
NAME_OFFSET = Leptris::XML::FFI::PullEventStruct.offset_of(:name)
|
|
138
|
+
TEXT_OFFSET = Leptris::XML::FFI::PullEventStruct.offset_of(:text)
|
|
139
|
+
private_constant :NAME_OFFSET, :TEXT_OFFSET
|
|
140
|
+
|
|
141
|
+
# Advances the cursor and returns the next Event, or nil after the
|
|
142
|
+
# end of the document. Attribute values are captured during
|
|
143
|
+
# start_element because the C accessors are valid only for the
|
|
144
|
+
# most recently returned event.
|
|
145
|
+
def next_event
|
|
146
|
+
raw = Leptris::XML::FFI.leptris_pull_next(@handle)
|
|
147
|
+
return nil if raw.null?
|
|
148
|
+
type = TYPES[raw.get_int(0)]
|
|
149
|
+
name_ptr = raw.get_pointer(NAME_OFFSET)
|
|
150
|
+
text_ptr = raw.get_pointer(TEXT_OFFSET)
|
|
151
|
+
attrs = type == :start_element ? capture_attrs : nil
|
|
152
|
+
text = text_ptr.null? ? nil :
|
|
153
|
+
text_ptr.read_string.force_encoding(Encoding::UTF_8)
|
|
154
|
+
text = Leptris::XML::FFI.read_pi_data(text) if type == :pi
|
|
155
|
+
Event.new(
|
|
156
|
+
type,
|
|
157
|
+
name_ptr.null? ? nil : name_ptr.read_string.force_encoding(Encoding::UTF_8),
|
|
158
|
+
text, attrs
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# One count-only query plus one flat copy (leptris_pull_attrs,
|
|
165
|
+
# 1.9.7) — the char** wire format the SAX callbacks already use —
|
|
166
|
+
# replaces the 2N per-index dispatches.
|
|
167
|
+
def capture_attrs
|
|
168
|
+
total = Leptris::XML::FFI.leptris_pull_attrs(@handle, nil, 0)
|
|
169
|
+
return nil if total.zero?
|
|
170
|
+
buffer = Leptris::XML::FFI.scratch_pointers(2 * total)
|
|
171
|
+
copied = Leptris::XML::FFI.leptris_pull_attrs(@handle, buffer, total)
|
|
172
|
+
ptr_size = ::FFI.type_size(:pointer)
|
|
173
|
+
hash = {}
|
|
174
|
+
i = 0
|
|
175
|
+
while i < copied
|
|
176
|
+
name = buffer.get_pointer(2 * i * ptr_size).read_string
|
|
177
|
+
.force_encoding(Encoding::UTF_8)
|
|
178
|
+
value = buffer.get_pointer((2 * i + 1) * ptr_size).read_string
|
|
179
|
+
.force_encoding(Encoding::UTF_8)
|
|
180
|
+
hash[name] = value
|
|
181
|
+
i += 1
|
|
182
|
+
end
|
|
183
|
+
hash
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def self.parse(xml_or_io, &block)
|
|
188
|
+
parser = Parser.parse(xml_or_io)
|
|
189
|
+
if block
|
|
190
|
+
begin
|
|
191
|
+
parser.each(&block)
|
|
192
|
+
ensure
|
|
193
|
+
parser.free
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
parser
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.parse_file(path, &block)
|
|
201
|
+
parser = Parser.parse_file(path)
|
|
202
|
+
if block
|
|
203
|
+
begin
|
|
204
|
+
parser.each(&block)
|
|
205
|
+
ensure
|
|
206
|
+
parser.free
|
|
207
|
+
end
|
|
208
|
+
else
|
|
209
|
+
parser
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Leptris::XML::RelaxNG
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse(schema_xml)
|
|
9
|
+
Schema.parse(schema_xml)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse_file(path)
|
|
13
|
+
Schema.parse_file(path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A compiled RELAX NG schema (libleptris >= 1.9.115): Jing-parity
|
|
17
|
+
# validation against the standard DOM — parse once, validate any
|
|
18
|
+
# number of documents. Schema errors publish to the thread-global
|
|
19
|
+
# error channel, so failures raise with the engine's detail
|
|
20
|
+
# ("unknown pattern element: ...", "cannot open schema file ...").
|
|
21
|
+
#
|
|
22
|
+
# rng = Leptris::XML::RelaxNG.parse(schema_xml)
|
|
23
|
+
# rng.valid?(doc) # => true/false
|
|
24
|
+
# rng.validate(doc) # => [] or ["1:0: error: element ..."]
|
|
25
|
+
# rng.validate_errors(doc) # => [] or [{line:, column:, message:}]
|
|
26
|
+
# rng.validate_report(doc) # => [] or [{kind:, message:, offender:, ...}]
|
|
27
|
+
#
|
|
28
|
+
class Schema
|
|
29
|
+
# GC-managed compiled handle.
|
|
30
|
+
class Handle < ::FFI::AutoPointer
|
|
31
|
+
def self.release(ptr)
|
|
32
|
+
Leptris::XML::FFI.leptris_rng_free(ptr)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Compile a schema from XML text. Raises Error on a schema
|
|
37
|
+
# error (detail via leptris_last_error).
|
|
38
|
+
def self.parse(schema_xml)
|
|
39
|
+
xml = schema_xml.to_s
|
|
40
|
+
raw = Leptris::XML::FFI.leptris_rng_parse(xml, xml.bytesize, nil)
|
|
41
|
+
if raw.null?
|
|
42
|
+
raise Leptris::XML::Error,
|
|
43
|
+
"schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
|
|
44
|
+
end
|
|
45
|
+
new(Handle.new(raw))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Compile from a file; <include href="..."> resolves relative to
|
|
49
|
+
# the file's directory (Jing's override rules apply).
|
|
50
|
+
def self.parse_file(path)
|
|
51
|
+
raw = Leptris::XML::FFI.leptris_rng_parse_file(path.to_s, nil)
|
|
52
|
+
if raw.null?
|
|
53
|
+
raise Leptris::XML::Error,
|
|
54
|
+
"schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
|
|
55
|
+
end
|
|
56
|
+
new(Handle.new(raw))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(handle)
|
|
60
|
+
@handle = handle
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Nokogiri shape: an array of error strings (the engine emits
|
|
64
|
+
# Jing's "line:col: error: message" form), empty when valid.
|
|
65
|
+
# libleptris 1.9.179 (#878): errors accumulate — every failure
|
|
66
|
+
# from the validate call is returned, not just the first.
|
|
67
|
+
def validate(document)
|
|
68
|
+
structured = validate_errors(document)
|
|
69
|
+
unless structured.empty?
|
|
70
|
+
return structured.map do |e|
|
|
71
|
+
e[:line] > 0 ? "#{e[:line]}:#{e[:column]}: error: #{e[:message]}"
|
|
72
|
+
: e[:message]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
# Invalid verdict with nothing accumulated: the back-compat
|
|
76
|
+
# single-error accessor is the last resort (nil-safe).
|
|
77
|
+
return [] if valid?(document)
|
|
78
|
+
[Leptris::XML::FFI.leptris_rng_error(@handle)].compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def valid?(document)
|
|
82
|
+
Leptris::XML::FFI.leptris_rng_validate(@handle,
|
|
83
|
+
document.c_ptr) != 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Structured errors: [{ line:, column:, message: }] — the fields
|
|
87
|
+
# callers need for log formatting, instead of re-parsing the
|
|
88
|
+
# Jing-form strings from #validate. Empty when valid. Message,
|
|
89
|
+
# line, and column carry Jing's exact attribution (libleptris
|
|
90
|
+
# >= 1.9.179, upstream #878). Rendered from #validate_report —
|
|
91
|
+
# the single enumeration of the error surface.
|
|
92
|
+
def validate_errors(document)
|
|
93
|
+
validate_report(document).map do |r|
|
|
94
|
+
{ line: r[:line], column: r[:column], message: r[:message] }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The whole validation report, one C call (libleptris >= 1.9.190):
|
|
99
|
+
# [{ kind:, message:, offender:, line:, column: }] — empty when
|
|
100
|
+
# valid. `kind` is the #1126 failure-class taxonomy
|
|
101
|
+
# ("missing-required-attr", "attr-not-allowed", ...), `offender`
|
|
102
|
+
# the attributed element/attribute name (nil when unknown).
|
|
103
|
+
# Strings are read into Ruby copies before returning.
|
|
104
|
+
def validate_report(document)
|
|
105
|
+
ok = Leptris::XML::FFI.leptris_rng_validate(@handle,
|
|
106
|
+
document.c_ptr)
|
|
107
|
+
return [] if ok != 0
|
|
108
|
+
out = ::FFI::MemoryPointer.new(:pointer)
|
|
109
|
+
count = Leptris::XML::FFI.leptris_rng_error_report(@handle, out)
|
|
110
|
+
return [] if count.zero?
|
|
111
|
+
base = out.read_pointer
|
|
112
|
+
return [] if base.null?
|
|
113
|
+
record = Leptris::XML::FFI::RngErrorRecord
|
|
114
|
+
Array.new(count) do |i|
|
|
115
|
+
r = record.new(base + i * record.size)
|
|
116
|
+
{
|
|
117
|
+
kind: r[:kind],
|
|
118
|
+
message: r[:message],
|
|
119
|
+
offender: r[:offender],
|
|
120
|
+
line: r[:line],
|
|
121
|
+
column: r[:column],
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Attribute-kind item from an XPath result set (ruby#153): the
|
|
4
|
+
# engine exposes synthetic attribute nodes whose name and value are
|
|
5
|
+
# readable only through the live result handle — the NodeSet
|
|
6
|
+
# captures both at materialization time and hands them to this
|
|
7
|
+
# wrapper, so the faces stay usable after the result is freed.
|
|
8
|
+
#
|
|
9
|
+
# doc.xpath("//item/@id").first.name # => "id"
|
|
10
|
+
# doc.xpath("//item/@id").first.value # => "1"
|
|
11
|
+
#
|
|
12
|
+
class Leptris::XML::ResultAttr < Leptris::XML::Node
|
|
13
|
+
def initialize(c_ptr, document, name, value, parent: nil, node_type: nil)
|
|
14
|
+
super(c_ptr, document, parent: parent, node_type: node_type)
|
|
15
|
+
@attr_name = name
|
|
16
|
+
@attr_value = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
@attr_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Nokogiri's Attr#value is the attribute's string value.
|
|
24
|
+
def value
|
|
25
|
+
@attr_value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def content
|
|
29
|
+
@attr_value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
@attr_value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def attribute?
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Text-kind item from an XPath/XQuery sequence result: the engine
|
|
4
|
+
# materializes sequence, map, and array items as synthetic text
|
|
5
|
+
# nodes (leptris/types.h's internal kind 8) whose value is readable
|
|
6
|
+
# only through the live result handle — the node-pointer accessors
|
|
7
|
+
# return nothing for them. The NodeSet therefore captures the string
|
|
8
|
+
# value at materialization time and hands it to this wrapper.
|
|
9
|
+
#
|
|
10
|
+
# doc.xpath("for $w in //a return string($w/@v)").map(&:content)
|
|
11
|
+
# # => ["1", "2"]
|
|
12
|
+
#
|
|
13
|
+
class Leptris::XML::ResultText < Leptris::XML::Node
|
|
14
|
+
def initialize(c_ptr, document, value, parent: nil, node_type: nil)
|
|
15
|
+
super(c_ptr, document, parent: parent, node_type: node_type)
|
|
16
|
+
@value = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
"text"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The captured string: safe to read even after the owning result
|
|
24
|
+
# set has been collected (unlike pointer-backed reads).
|
|
25
|
+
def content
|
|
26
|
+
@value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
@value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def text?
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::SAX::Document
|
|
4
|
+
# Called when an XML declaration is parsed.
|
|
5
|
+
def xmldecl(version, encoding, standalone)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def start_document
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def end_document
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Called at the beginning of an element.
|
|
15
|
+
# +attrs+ is an Array of [name, value] pairs in source order.
|
|
16
|
+
def start_element(name, attrs = [])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def end_element(name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def characters(string)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def comment(string)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cdata_block(string)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def processing_instruction(name, content)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def start_prefix_mapping(prefix, uri)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def end_prefix_mapping(prefix)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warning(string)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error(message, line = 0, column = 0)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DOM-backed SAX dispatch (leptris-ruby#95): the engine's streaming
|
|
4
|
+
# attribute buffer corrupts leading attribute pairs when several
|
|
5
|
+
# nested ancestor levels each carry attributes — the callback
|
|
6
|
+
# transport, the recorder's packed arena, AND the pull parser all
|
|
7
|
+
# deliver the loss on the issue fixture (verified), while the DOM
|
|
8
|
+
# parser reads the same bytes correctly. Until the engine fix
|
|
9
|
+
# lands, SAX::Parser delivers from a DOM parse: same call shapes as
|
|
10
|
+
# the streaming transports — attach-only-overridden kinds,
|
|
11
|
+
# one-argument start_element arity, QName element names ("p:a"),
|
|
12
|
+
# UTF-8 strings, PI-data normalization.
|
|
13
|
+
#
|
|
14
|
+
# The walk is lean by construction: elements wrap (identity, name
|
|
15
|
+
# and prefix memos); every other child kind is read straight from
|
|
16
|
+
# its pointer — content getters per node type, never a wrapper.
|
|
17
|
+
module Leptris::XML::SAX::DomDispatch
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def parse(handler, dispatched, string)
|
|
21
|
+
doc = Leptris::XML::Document.parse(string)
|
|
22
|
+
begin
|
|
23
|
+
dispatch(handler, dispatched, doc)
|
|
24
|
+
ensure
|
|
25
|
+
doc.free
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dispatch(handler, dispatched, doc)
|
|
30
|
+
handler.start_document if dispatched[:start_document]
|
|
31
|
+
doc_node = doc.node
|
|
32
|
+
if doc_node
|
|
33
|
+
visit_children(doc_node.c_ptr, doc, handler, dispatched,
|
|
34
|
+
element_parent: false)
|
|
35
|
+
end
|
|
36
|
+
handler.end_document if dispatched[:end_document]
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
NODE = Leptris::XML::FFI
|
|
41
|
+
ELEMENT = Leptris::XML::FFI::NODE_ELEMENT
|
|
42
|
+
TEXT = Leptris::XML::FFI::NODE_TEXT
|
|
43
|
+
COMMENT = Leptris::XML::FFI::NODE_COMMENT
|
|
44
|
+
CDATA = Leptris::XML::FFI::NODE_CDATA
|
|
45
|
+
PI = Leptris::XML::FFI::NODE_PI
|
|
46
|
+
|
|
47
|
+
def visit_children(c_ptr, doc, handler, dispatched, element_parent: true)
|
|
48
|
+
wants_text_kinds = dispatched[:characters] ||
|
|
49
|
+
dispatched[:comment] ||
|
|
50
|
+
dispatched[:cdata] || dispatched[:pi]
|
|
51
|
+
if element_parent && !wants_text_kinds
|
|
52
|
+
# Element-only handlers under an element: the element batch
|
|
53
|
+
# hands out element pointers with no per-child get_type
|
|
54
|
+
# (round XXIII's path) — text/comments/PIs are never fetched.
|
|
55
|
+
# (The document node is not an element handle — the typed
|
|
56
|
+
# walk serves it.)
|
|
57
|
+
Leptris::XML::FFI.fetch_element_children(c_ptr).each do |child_ptr|
|
|
58
|
+
visit_element(
|
|
59
|
+
Leptris::XML::Node.wrap(child_ptr, doc, node_type: ELEMENT),
|
|
60
|
+
handler, dispatched)
|
|
61
|
+
end
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
|
|
65
|
+
pointers.each_with_index do |child_ptr, i|
|
|
66
|
+
case kinds[i]
|
|
67
|
+
when ELEMENT
|
|
68
|
+
visit_element(
|
|
69
|
+
Leptris::XML::Node.wrap(child_ptr, doc, node_type: ELEMENT),
|
|
70
|
+
handler, dispatched)
|
|
71
|
+
when TEXT
|
|
72
|
+
if dispatched[:characters]
|
|
73
|
+
handler.characters(utf8(NODE.leptris_text_node_get_content(child_ptr)))
|
|
74
|
+
end
|
|
75
|
+
when CDATA
|
|
76
|
+
if dispatched[:cdata]
|
|
77
|
+
handler.cdata_block(utf8(NODE.leptris_cdata_node_get_content(child_ptr)))
|
|
78
|
+
end
|
|
79
|
+
when COMMENT
|
|
80
|
+
if dispatched[:comment]
|
|
81
|
+
handler.comment(utf8(NODE.leptris_comment_node_get_content(child_ptr)))
|
|
82
|
+
end
|
|
83
|
+
when PI
|
|
84
|
+
if dispatched[:pi]
|
|
85
|
+
handler.processing_instruction(
|
|
86
|
+
utf8(NODE.leptris_pi_node_get_target(child_ptr)),
|
|
87
|
+
Leptris::XML::FFI.read_pi_data(
|
|
88
|
+
utf8(NODE.leptris_pi_node_get_data(child_ptr))))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def visit_element(element, handler, dispatched)
|
|
95
|
+
qname = element.prefix ? "#{element.prefix}:#{element.name}" : element.name
|
|
96
|
+
if dispatched[:start_prefix]
|
|
97
|
+
element.namespace_definitions.each do |ns|
|
|
98
|
+
handler.start_prefix_mapping(ns.prefix.to_s, ns.href.to_s)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
if dispatched[:start_element]
|
|
102
|
+
if dispatched[:start_element] == :one_arg
|
|
103
|
+
handler.start_element(qname)
|
|
104
|
+
else
|
|
105
|
+
handler.start_element(qname, declared_pairs(element))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
visit_children(element.c_ptr, element.document, handler, dispatched)
|
|
109
|
+
handler.end_element(qname) if dispatched[:end_element]
|
|
110
|
+
if dispatched[:end_prefix]
|
|
111
|
+
element.namespace_definitions.reverse_each do |ns|
|
|
112
|
+
handler.end_prefix_mapping(ns.prefix.to_s)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# start_element's pairs with xmlns declarations carried — the
|
|
118
|
+
# engine's streaming contract reports declarations among the
|
|
119
|
+
# attribute pairs at their byte positions. Since libleptris
|
|
120
|
+
# 1.9.18 (upstream #635, closing leptris-ruby#99's documented
|
|
121
|
+
# difference), the raw qname-ordered list reproduces the exact
|
|
122
|
+
# interleave.
|
|
123
|
+
def declared_pairs(element)
|
|
124
|
+
Leptris::XML::FFI.fetch_attributes_raw(element.c_ptr)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# [name, value] pairs in source order, read straight from the
|
|
128
|
+
# attribute chain — no Attr objects, no hash.
|
|
129
|
+
def attr_pairs(c_ptr)
|
|
130
|
+
pairs = []
|
|
131
|
+
attr = Leptris::XML::FFI.leptris_element_first_attribute(c_ptr)
|
|
132
|
+
until attr.nil? || attr.null?
|
|
133
|
+
pairs << [utf8(Leptris::XML::FFI.leptris_attribute_get_name(attr)),
|
|
134
|
+
utf8(Leptris::XML::FFI.leptris_attribute_get_value(
|
|
135
|
+
c_ptr, attr))]
|
|
136
|
+
attr = Leptris::XML::FFI.leptris_attribute_next(attr)
|
|
137
|
+
end
|
|
138
|
+
pairs
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def utf8(str)
|
|
142
|
+
str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
|
|
143
|
+
end
|
|
144
|
+
end
|