leptris 1.5.0-aarch64-linux → 1.6.1-aarch64-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 +86 -0
- data/CLAUDE.md +76 -87
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/cdata.rb +1 -0
- data/lib/leptris/xml/comment.rb +1 -0
- data/lib/leptris/xml/document.rb +47 -14
- data/lib/leptris/xml/element.rb +21 -3
- data/lib/leptris/xml/ffi.rb +70 -13
- data/lib/leptris/xml/iterparse.rb +57 -0
- data/lib/leptris/xml/node.rb +32 -10
- data/lib/leptris/xml/node_set.rb +6 -5
- data/lib/leptris/xml/processing_instruction.rb +2 -0
- data/lib/leptris/xml/pull.rb +123 -0
- data/lib/leptris/xml/searchable.rb +5 -2
- data/lib/leptris/xml/serialization.rb +20 -2
- data/lib/leptris/xml/text.rb +1 -0
- data/lib/leptris/xml/xpath.rb +80 -0
- data/lib/leptris/xml.rb +6 -2
- data/lib/libleptris.so +0 -0
- metadata +5 -37
- data/TODO.impl/01-architecture.md +0 -217
- data/TODO.impl/02-ffi-declarations.md +0 -236
- data/TODO.impl/03-document-node-element-nodeset.md +0 -382
- data/TODO.impl/04-sax-parser.md +0 -203
- data/TODO.impl/05-serialize-c14n-memory-specs-css.md +0 -276
- data/docs/ARCHITECTURE.adoc +0 -559
- data/docs/BUILD.md +0 -395
- data/docs/ERROR_MESSAGES.md +0 -458
- data/docs/FFI_ARCHITECTURE.md +0 -439
- data/docs/FUTURE_VISION.md +0 -303
- data/docs/GITHUB_ACTIONS.md +0 -293
- data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +0 -459
- data/docs/PERFORMANCE.adoc +0 -668
- data/docs/PERFORMANCE.md +0 -448
- data/docs/RELEASE_NOTES_v1.0.0.md +0 -515
- data/docs/XPATH_SPEC_COMPLIANCE.md +0 -298
- data/docs/completion/leptris.bash +0 -86
- data/docs/completion/leptris.zsh +0 -74
- data/docs/man/leptris-format.1 +0 -227
- data/docs/man/leptris-parse.1 +0 -178
- data/docs/man/leptris-xpath.1 +0 -312
- data/docs/man/leptris.1 +0 -160
- data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +0 -217
- data/docs/v0.9.0_RELEASE_SUMMARY.md +0 -281
- data/docs/v1.0.0_CONTINUATION_PLAN.md +0 -172
- data/docs/v1.0.0_CONTINUATION_PROMPT.md +0 -382
- data/docs/v1.0.0_SESSION_6_CONTINUATION.md +0 -434
- data/docs/v1.0.0_SESSION_6_PROMPT.md +0 -231
- data/docs/v1.0.0_STATUS_TRACKER.md +0 -224
- data/docs/v1.1.0_CONTINUATION_PLAN.md +0 -299
- data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +0 -201
- data/docs/v1.1.0_SESSION_3_PROMPT.md +0 -223
- data/docs/v1.1.0_STATUS_TRACKER.md +0 -355
- data/docs/xml-performance.adoc +0 -115
- data/docs/xpath-performance.adoc +0 -379
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -24,6 +24,9 @@ module Leptris
|
|
|
24
24
|
typedef :pointer, :leptris_doctype
|
|
25
25
|
typedef :pointer, :leptris_xpath_result
|
|
26
26
|
typedef :pointer, :leptris_xpath_ns_set
|
|
27
|
+
typedef :pointer, :leptris_xpath_compiled
|
|
28
|
+
typedef :pointer, :leptris_pull_parser
|
|
29
|
+
typedef :pointer, :leptris_iterparse
|
|
27
30
|
typedef :pointer, :leptris_xpath_var_set
|
|
28
31
|
typedef :pointer, :leptris_sax_parser
|
|
29
32
|
typedef :int, :leptris_status
|
|
@@ -73,6 +76,20 @@ module Leptris
|
|
|
73
76
|
[:leptris_document, :leptris_element], :leptris_status
|
|
74
77
|
attach_function :leptris_document_root,
|
|
75
78
|
[:leptris_document], :leptris_element
|
|
79
|
+
# Document-level PIs (v1.6.0): not tree nodes — enumerate via
|
|
80
|
+
# these accessors only.
|
|
81
|
+
attach_function :leptris_document_pi_count,
|
|
82
|
+
[:leptris_document], :size_t
|
|
83
|
+
attach_function :leptris_document_pi_target,
|
|
84
|
+
[:leptris_document, :size_t], :string
|
|
85
|
+
attach_function :leptris_document_pi_data,
|
|
86
|
+
[:leptris_document, :size_t], :string
|
|
87
|
+
attach_function :leptris_document_add_pi,
|
|
88
|
+
[:leptris_document, :string, :string], :pointer
|
|
89
|
+
# Options-struct parse (v1.6.0): supersedes the flags variants;
|
|
90
|
+
# the flags path remains bound for compatibility.
|
|
91
|
+
attach_function :leptris_parse_string_ex,
|
|
92
|
+
[:string, :size_t, :pointer, :pointer], :leptris_document
|
|
76
93
|
attach_function :leptris_document_encoding,
|
|
77
94
|
[:leptris_document], :string
|
|
78
95
|
attach_function :leptris_document_finalize_strings,
|
|
@@ -324,8 +341,6 @@ module Leptris
|
|
|
324
341
|
[:leptris_xpath_result], :int
|
|
325
342
|
attach_function :leptris_xpath_result_count,
|
|
326
343
|
[:leptris_xpath_result], :size_t
|
|
327
|
-
attach_function :leptris_xpath_result_get,
|
|
328
|
-
[:leptris_xpath_result, :size_t], :leptris_element
|
|
329
344
|
# Mixed nodesets (v1.1.0): unlike result_get (elements only),
|
|
330
345
|
# get_node returns the node whatever its kind; node_kind reports
|
|
331
346
|
# which (element / synthetic attribute / text / other).
|
|
@@ -342,10 +357,8 @@ module Leptris
|
|
|
342
357
|
# a heap string the library frees.
|
|
343
358
|
attach_function :leptris_xpath_register_function,
|
|
344
359
|
[:leptris_document, :string, :pointer, :pointer], :leptris_status
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
# v1.3.0: copies ALL node kinds (the legacy accessor drops
|
|
348
|
-
# non-element entries) and can also report per-entry kinds.
|
|
360
|
+
# v1.3.0: copies ALL node kinds in one call, optionally
|
|
361
|
+
# reporting per-entry kinds.
|
|
349
362
|
attach_function :leptris_xpath_result_get_nodes_ex,
|
|
350
363
|
[:leptris_xpath_result, :pointer, :pointer, :size_t], :size_t
|
|
351
364
|
attach_function :leptris_xpath_result_boolean,
|
|
@@ -376,15 +389,27 @@ module Leptris
|
|
|
376
389
|
[], :leptris_xpath_ns_set
|
|
377
390
|
attach_function :leptris_xpath_ns_set_free,
|
|
378
391
|
[:leptris_xpath_ns_set], :void
|
|
379
|
-
attach_function :leptris_xpath_ns_set_add,
|
|
380
|
-
[:leptris_xpath_ns_set, :string, :string], :leptris_status
|
|
381
392
|
# One-call constructor: flat array of 2*pair_count alternating
|
|
382
|
-
# prefix/URI strings
|
|
393
|
+
# prefix/URI strings.
|
|
383
394
|
attach_function :leptris_xpath_ns_set_new_from_pairs,
|
|
384
395
|
[:pointer, :size_t], :leptris_xpath_ns_set
|
|
385
396
|
attach_function :leptris_xpath_eval_ns,
|
|
386
397
|
[:leptris_document, :leptris_element, :string, :leptris_xpath_ns_set],
|
|
387
398
|
:leptris_xpath_result
|
|
399
|
+
# Compiled expressions (v1.6.0): parse once, evaluate many.
|
|
400
|
+
attach_function :leptris_xpath_compile,
|
|
401
|
+
[:string], :leptris_xpath_compiled
|
|
402
|
+
attach_function :leptris_xpath_compiled_eval,
|
|
403
|
+
[:leptris_xpath_compiled, :leptris_document, :leptris_element],
|
|
404
|
+
:leptris_xpath_result
|
|
405
|
+
attach_function :leptris_xpath_compiled_eval_ns,
|
|
406
|
+
[:leptris_xpath_compiled, :leptris_document, :leptris_element,
|
|
407
|
+
:leptris_xpath_ns_set], :leptris_xpath_result
|
|
408
|
+
attach_function :leptris_xpath_compiled_eval_vars,
|
|
409
|
+
[:leptris_xpath_compiled, :leptris_document, :leptris_element,
|
|
410
|
+
:leptris_xpath_var_set], :leptris_xpath_result
|
|
411
|
+
attach_function :leptris_xpath_compiled_free,
|
|
412
|
+
[:leptris_xpath_compiled], :void
|
|
388
413
|
|
|
389
414
|
attach_function :leptris_sax_parse,
|
|
390
415
|
[:string, :size_t, :pointer, :pointer], :int
|
|
@@ -397,12 +422,35 @@ module Leptris
|
|
|
397
422
|
attach_function :leptris_sax_parser_set_streaming,
|
|
398
423
|
[:leptris_sax_parser, :int], :int
|
|
399
424
|
|
|
425
|
+
# Pull parsing (v1.6.0): StAX-style cursor over a document.
|
|
426
|
+
attach_function :leptris_pull_new,
|
|
427
|
+
[:string, :size_t], :leptris_pull_parser
|
|
428
|
+
attach_function :leptris_pull_new_file,
|
|
429
|
+
[:string], :leptris_pull_parser
|
|
430
|
+
attach_function :leptris_pull_next,
|
|
431
|
+
[:leptris_pull_parser], :pointer
|
|
432
|
+
attach_function :leptris_pull_attr_count,
|
|
433
|
+
[:leptris_pull_parser], :size_t
|
|
434
|
+
attach_function :leptris_pull_attr_name,
|
|
435
|
+
[:leptris_pull_parser, :size_t], :string
|
|
436
|
+
attach_function :leptris_pull_attr_value,
|
|
437
|
+
[:leptris_pull_parser, :size_t], :string
|
|
438
|
+
attach_function :leptris_pull_free,
|
|
439
|
+
[:leptris_pull_parser], :void
|
|
440
|
+
|
|
441
|
+
# Iterparse (v1.6.0): yields each completed top-level child
|
|
442
|
+
# element; the previous subtree is released on each next call.
|
|
443
|
+
attach_function :leptris_iterparse_new,
|
|
444
|
+
[:string, :size_t], :leptris_iterparse
|
|
445
|
+
attach_function :leptris_iterparse_new_file,
|
|
446
|
+
[:string], :leptris_iterparse
|
|
447
|
+
attach_function :leptris_iterparse_next,
|
|
448
|
+
[:leptris_iterparse], :leptris_element
|
|
449
|
+
attach_function :leptris_iterparse_free,
|
|
450
|
+
[:leptris_iterparse], :void
|
|
451
|
+
|
|
400
452
|
attach_function :leptris_document_serialize,
|
|
401
453
|
[:leptris_document, :pointer], :pointer
|
|
402
|
-
# Legacy simple serializer, declared retroactively in v1.1.0.
|
|
403
|
-
# Prefer leptris_document_serialize; bound for API completeness.
|
|
404
|
-
attach_function :leptris_serialize_document,
|
|
405
|
-
[:leptris_document], :pointer
|
|
406
454
|
attach_function :leptris_document_get_dtd,
|
|
407
455
|
[:leptris_document], :pointer
|
|
408
456
|
attach_function :leptris_element_serialize,
|
|
@@ -491,6 +539,15 @@ module Leptris
|
|
|
491
539
|
TRAVERSE_PRE_ORDER = 0
|
|
492
540
|
TRAVERSE_POST_ORDER = 1
|
|
493
541
|
|
|
542
|
+
PULL_START_ELEMENT = 0
|
|
543
|
+
PULL_END_ELEMENT = 1
|
|
544
|
+
PULL_TEXT = 2
|
|
545
|
+
PULL_COMMENT = 3
|
|
546
|
+
PULL_CDATA = 4
|
|
547
|
+
PULL_PI = 5
|
|
548
|
+
PULL_END_DOCUMENT = 6
|
|
549
|
+
PULL_ERROR = 7
|
|
550
|
+
|
|
494
551
|
LEPTRIS_PARSE_DEFAULT = 0
|
|
495
552
|
LEPTRIS_PARSE_DROP_WS_TEXT = 1
|
|
496
553
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
# Incremental tree iteration (libleptris v1.6.0): yields each
|
|
6
|
+
# completed TOP-LEVEL child element of the document root as it
|
|
7
|
+
# parses. The previous element's whole subtree is released when the
|
|
8
|
+
# next is produced — memory stays bounded by the largest subtree,
|
|
9
|
+
# not the document.
|
|
10
|
+
#
|
|
11
|
+
# Leptris::XML::Iterparse.parse(huge_xml) do |element|
|
|
12
|
+
# process(element) # subtree valid until the next iteration
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# v1 limitation (upstream): element names are the QName as written;
|
|
16
|
+
# namespace prefixes are not re-resolved — use the DOM path when
|
|
17
|
+
# namespace URIs matter. The yielded elements have no parent
|
|
18
|
+
# Document; #document returns nil and they must not outlive the
|
|
19
|
+
# iteration.
|
|
20
|
+
class Leptris::XML::Iterparse
|
|
21
|
+
def self.parse(xml_or_io, &block)
|
|
22
|
+
xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
|
|
23
|
+
iterator = new(Leptris::XML::FFI.leptris_iterparse_new(xml, xml.bytesize))
|
|
24
|
+
iterator.run(&block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.parse_file(path, &block)
|
|
28
|
+
iterator = new(Leptris::XML::FFI.leptris_iterparse_new_file(path))
|
|
29
|
+
iterator.run(&block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(handle)
|
|
33
|
+
raise Leptris::XML::ParseError, "leptris_iterparse_new failed" if handle.null?
|
|
34
|
+
@handle = handle
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def run
|
|
38
|
+
return enum_for(:run) unless block_given?
|
|
39
|
+
loop do
|
|
40
|
+
ptr = Leptris::XML::FFI.leptris_iterparse_next(@handle)
|
|
41
|
+
break if ptr.null?
|
|
42
|
+
# document: nil — the subtree belongs to the iterator's pool,
|
|
43
|
+
# not a long-lived Document, so wrapper caching is skipped and
|
|
44
|
+
# #document is nil on the yielded elements.
|
|
45
|
+
yield Leptris::XML::Node.wrap(ptr, nil)
|
|
46
|
+
end
|
|
47
|
+
self
|
|
48
|
+
ensure
|
|
49
|
+
free
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def free
|
|
53
|
+
return if @handle.nil?
|
|
54
|
+
Leptris::XML::FFI.leptris_iterparse_free(@handle)
|
|
55
|
+
@handle = nil
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
class Leptris::XML::Node
|
|
4
4
|
attr_reader :c_ptr, :document
|
|
5
5
|
|
|
6
|
-
def initialize(c_ptr, document, parent: nil)
|
|
6
|
+
def initialize(c_ptr, document, parent: nil, node_type: nil)
|
|
7
7
|
@c_ptr = c_ptr
|
|
8
8
|
@document = document
|
|
9
9
|
@parent = parent
|
|
10
|
+
# wrap() already calls leptris_node_get_type for dispatch; reusing
|
|
11
|
+
# the result makes every predicate and #type call FFI-free.
|
|
12
|
+
@node_type = node_type
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def self.wrap(c_ptr, document, parent: nil)
|
|
@@ -18,20 +21,21 @@ class Leptris::XML::Node
|
|
|
18
21
|
return cached
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
node_type = Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
21
25
|
node =
|
|
22
|
-
case
|
|
26
|
+
case node_type
|
|
23
27
|
when Leptris::XML::FFI::NODE_ELEMENT
|
|
24
|
-
Leptris::XML::Element.new(c_ptr, document, parent: parent)
|
|
28
|
+
Leptris::XML::Element.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
25
29
|
when Leptris::XML::FFI::NODE_TEXT
|
|
26
|
-
Leptris::XML::Text.new(c_ptr, document, parent: parent)
|
|
30
|
+
Leptris::XML::Text.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
27
31
|
when Leptris::XML::FFI::NODE_COMMENT
|
|
28
|
-
Leptris::XML::Comment.new(c_ptr, document, parent: parent)
|
|
32
|
+
Leptris::XML::Comment.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
29
33
|
when Leptris::XML::FFI::NODE_CDATA
|
|
30
|
-
Leptris::XML::CDATA.new(c_ptr, document, parent: parent)
|
|
34
|
+
Leptris::XML::CDATA.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
31
35
|
when Leptris::XML::FFI::NODE_PI
|
|
32
|
-
Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent)
|
|
36
|
+
Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
33
37
|
else
|
|
34
|
-
new(c_ptr, document, parent: parent)
|
|
38
|
+
new(c_ptr, document, parent: parent, node_type: node_type)
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
document.wrapper_cache[c_ptr.address] = node if document
|
|
@@ -58,7 +62,7 @@ class Leptris::XML::Node
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def type
|
|
61
|
-
Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
|
|
65
|
+
@node_type ||= Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
|
|
62
66
|
end
|
|
63
67
|
alias_method :node_type, :type
|
|
64
68
|
|
|
@@ -78,6 +82,14 @@ class Leptris::XML::Node
|
|
|
78
82
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
79
83
|
end
|
|
80
84
|
|
|
85
|
+
# Raises ReadOnlyError when the owning document was marked readonly.
|
|
86
|
+
def ensure_writable!
|
|
87
|
+
if @document&.readonly?
|
|
88
|
+
raise Leptris::XML::ReadOnlyError,
|
|
89
|
+
"document is readonly — mutation attempted on #{inspect}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
81
93
|
def line
|
|
82
94
|
Leptris::XML::FFI.leptris_node_line(@c_ptr)
|
|
83
95
|
end
|
|
@@ -95,13 +107,18 @@ class Leptris::XML::Node
|
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def children
|
|
110
|
+
# Immutable in readonly mode: the walk (first_child + one
|
|
111
|
+
# next_sibling per child) plus wrapper construction is paid once.
|
|
112
|
+
return @children if readonly_cached?(:@children)
|
|
98
113
|
nodes = []
|
|
99
114
|
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
100
115
|
until ptr.nil? || ptr.null?
|
|
101
116
|
nodes << Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
102
117
|
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
103
118
|
end
|
|
104
|
-
Leptris::XML::NodeSet.new(@document, nodes)
|
|
119
|
+
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
120
|
+
@children = result if @document&.readonly?
|
|
121
|
+
result
|
|
105
122
|
end
|
|
106
123
|
|
|
107
124
|
def next_sibling
|
|
@@ -150,6 +167,7 @@ class Leptris::XML::Node
|
|
|
150
167
|
end
|
|
151
168
|
|
|
152
169
|
def unlink
|
|
170
|
+
ensure_writable!
|
|
153
171
|
Leptris::XML::FFI.check_status(
|
|
154
172
|
Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
|
|
155
173
|
@parent = nil
|
|
@@ -212,6 +230,10 @@ class Leptris::XML::Node
|
|
|
212
230
|
|
|
213
231
|
protected
|
|
214
232
|
|
|
233
|
+
def readonly_cached?(ivar)
|
|
234
|
+
@document&.readonly? && instance_variable_defined?(ivar)
|
|
235
|
+
end
|
|
236
|
+
|
|
215
237
|
def as_element_or_self
|
|
216
238
|
is_a?(Leptris::XML::Element) ? self : nil
|
|
217
239
|
end
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -63,15 +63,16 @@ class Leptris::XML::NodeSet
|
|
|
63
63
|
def each
|
|
64
64
|
return enum_for(:each) unless block_given?
|
|
65
65
|
if @result_ptr
|
|
66
|
-
# Batch-fetch all node pointers in one FFI call
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# Node.wrap, so
|
|
66
|
+
# Batch-fetch all node pointers in one FFI call
|
|
67
|
+
# (leptris_xpath_result_get_nodes_ex copies every node kind, not
|
|
68
|
+
# just elements) and wrap each. Wrappers are cached per-Document
|
|
69
|
+
# via Node.wrap, so re-iteration of the same NodeSet hits it.
|
|
70
70
|
n = length
|
|
71
71
|
if n > 0
|
|
72
72
|
buf = ::FFI::MemoryPointer.new(:pointer, n)
|
|
73
73
|
begin
|
|
74
|
-
copied = Leptris::XML::FFI.
|
|
74
|
+
copied = Leptris::XML::FFI.leptris_xpath_result_get_nodes_ex(
|
|
75
|
+
@result_ptr, buf, nil, n)
|
|
75
76
|
copied.times do |i|
|
|
76
77
|
ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
|
|
77
78
|
next if ptr.null?
|
|
@@ -11,12 +11,14 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def target=(new_target)
|
|
14
|
+
ensure_writable!
|
|
14
15
|
Leptris::XML::FFI.check_status(
|
|
15
16
|
Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s))
|
|
16
17
|
new_target
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def data=(new_data)
|
|
21
|
+
ensure_writable!
|
|
20
22
|
Leptris::XML::FFI.check_status(
|
|
21
23
|
Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s))
|
|
22
24
|
new_data
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
Event = Struct.new(:type, :name, :text, :attrs, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
TYPES = {
|
|
20
|
+
Leptris::XML::FFI::PULL_START_ELEMENT => :start_element,
|
|
21
|
+
Leptris::XML::FFI::PULL_END_ELEMENT => :end_element,
|
|
22
|
+
Leptris::XML::FFI::PULL_TEXT => :text,
|
|
23
|
+
Leptris::XML::FFI::PULL_COMMENT => :comment,
|
|
24
|
+
Leptris::XML::FFI::PULL_CDATA => :cdata,
|
|
25
|
+
Leptris::XML::FFI::PULL_PI => :pi,
|
|
26
|
+
Leptris::XML::FFI::PULL_END_DOCUMENT => :end_document,
|
|
27
|
+
Leptris::XML::FFI::PULL_ERROR => :error,
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
class Parser
|
|
31
|
+
def self.parse(xml_or_io)
|
|
32
|
+
xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
|
|
33
|
+
new(Leptris::XML::FFI.leptris_pull_new(xml, xml.bytesize))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.parse_file(path)
|
|
37
|
+
new(Leptris::XML::FFI.leptris_pull_new_file(path))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(handle)
|
|
41
|
+
raise Leptris::XML::ParseError, "leptris_pull_new failed" if handle.null?
|
|
42
|
+
@handle = handle
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Releases the C parser. Safe to call twice.
|
|
46
|
+
def free
|
|
47
|
+
return if @handle.nil?
|
|
48
|
+
Leptris::XML::FFI.leptris_pull_free(@handle)
|
|
49
|
+
@handle = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def each
|
|
53
|
+
return enum_for(:each) unless block_given?
|
|
54
|
+
loop do
|
|
55
|
+
event = next_event
|
|
56
|
+
break if event.nil?
|
|
57
|
+
yield event
|
|
58
|
+
break if event.type == :end_document || event.type == :error
|
|
59
|
+
end
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Advances the cursor and returns the next Event, or nil after the
|
|
64
|
+
# end of the document. Attribute values are captured during
|
|
65
|
+
# start_element because the C accessors are valid only for the
|
|
66
|
+
# most recently returned event.
|
|
67
|
+
def next_event
|
|
68
|
+
raw = Leptris::XML::FFI.leptris_pull_next(@handle)
|
|
69
|
+
return nil if raw.null?
|
|
70
|
+
type_code = raw.get_int(0)
|
|
71
|
+
type = TYPES[type_code]
|
|
72
|
+
name = read_string_at(raw, 8)
|
|
73
|
+
text_ptr = raw.get_pointer(16)
|
|
74
|
+
text = text_ptr.null? ? nil : text_ptr.read_string
|
|
75
|
+
attrs = type == :start_element ? capture_attrs : nil
|
|
76
|
+
Event.new(type: type, name: name, text: text, attrs: attrs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# struct LeptrisPullEvent { int type; const char* name; const
|
|
82
|
+
# char* text; size_t text_len; } — pointer-sized fields follow the
|
|
83
|
+
# 4-byte enum on 64-bit ABIs (name at 8, text at 16).
|
|
84
|
+
def read_string_at(raw, offset)
|
|
85
|
+
ptr = raw.get_pointer(offset)
|
|
86
|
+
ptr.null? ? nil : ptr.read_string
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def capture_attrs
|
|
90
|
+
count = Leptris::XML::FFI.leptris_pull_attr_count(@handle)
|
|
91
|
+
count.times.each_with_object({}) do |i, hash|
|
|
92
|
+
hash[Leptris::XML::FFI.leptris_pull_attr_name(@handle, i)] =
|
|
93
|
+
Leptris::XML::FFI.leptris_pull_attr_value(@handle, i)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.parse(xml_or_io, &block)
|
|
99
|
+
parser = Parser.parse(xml_or_io)
|
|
100
|
+
if block
|
|
101
|
+
begin
|
|
102
|
+
parser.each(&block)
|
|
103
|
+
ensure
|
|
104
|
+
parser.free
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
parser
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.parse_file(path, &block)
|
|
112
|
+
parser = Parser.parse_file(path)
|
|
113
|
+
if block
|
|
114
|
+
begin
|
|
115
|
+
parser.each(&block)
|
|
116
|
+
ensure
|
|
117
|
+
parser.free
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
parser
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -20,7 +20,7 @@ module Leptris::XML::Searchable
|
|
|
20
20
|
Leptris::XML::FFI.leptris_status_string(Leptris::XML::FFI::LEPTRIS_ERROR_XPATH)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
wrap_xpath_result(result_ptr)
|
|
23
|
+
Leptris::XML::Searchable.wrap_xpath_result(document, result_ptr)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def at_xpath(*paths)
|
|
@@ -88,7 +88,10 @@ module Leptris::XML::Searchable
|
|
|
88
88
|
%r{\A(\./|/|\.\.|\.)}.match?(str)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
# Shared by Searchable#xpath and Leptris::XML::XPath (compiled
|
|
92
|
+
# expressions): wraps a raw XPathResult pointer into the
|
|
93
|
+
# Ruby-typed result and frees the C handle.
|
|
94
|
+
def self.wrap_xpath_result(document, result_ptr)
|
|
92
95
|
type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
|
|
93
96
|
case type
|
|
94
97
|
when Leptris::XML::FFI::XPATH_NODESET
|
|
@@ -6,11 +6,29 @@
|
|
|
6
6
|
# struct, the encoding anchor, the C call, and the read-and-free all
|
|
7
7
|
# live here.
|
|
8
8
|
module Leptris::XML::Serialization
|
|
9
|
+
# The default options struct (indent 0, declaration on, no encoding
|
|
10
|
+
# override) is constant: build it once and reuse its pointer on the
|
|
11
|
+
# fast path instead of allocating a fresh struct per call.
|
|
12
|
+
DEFAULT_OPTIONS, _default_anchor = begin
|
|
13
|
+
opts = Leptris::XML::FFI::SerializeOptions.new
|
|
14
|
+
opts[:indent] = 0
|
|
15
|
+
opts[:xml_declaration] = 1
|
|
16
|
+
opts[:encoding] = nil
|
|
17
|
+
[opts, nil]
|
|
18
|
+
end
|
|
19
|
+
private_constant :DEFAULT_OPTIONS
|
|
20
|
+
|
|
9
21
|
# +ffi_function+ is a bound FFI function taking (c_ptr, opts_ptr):
|
|
10
22
|
# leptris_document_serialize or leptris_element_serialize.
|
|
11
23
|
def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
|
|
12
|
-
opts
|
|
13
|
-
|
|
24
|
+
opts =
|
|
25
|
+
if indent.to_i.zero? && !no_decl && encoding.nil?
|
|
26
|
+
DEFAULT_OPTIONS
|
|
27
|
+
else
|
|
28
|
+
opts, _encoding_anchor = build_options(
|
|
29
|
+
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
30
|
+
opts
|
|
31
|
+
end
|
|
14
32
|
str_ptr = ffi_function.call(c_ptr, opts.pointer)
|
|
15
33
|
Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
16
34
|
end
|
data/lib/leptris/xml/text.rb
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
# Compiled XPath expression: parse once, evaluate many times against
|
|
6
|
+
# any document (libleptris v1.6.0). The C handle is released by GC.
|
|
7
|
+
#
|
|
8
|
+
# expr = Leptris::XML::XPath.compile("//item[@qty > 3]")
|
|
9
|
+
# expr.eval(doc_a).length
|
|
10
|
+
# expr.eval(doc_b, "p" => "urn:p").length
|
|
11
|
+
#
|
|
12
|
+
class Leptris::XML::XPath
|
|
13
|
+
class CompiledHandle < ::FFI::AutoPointer
|
|
14
|
+
def self.release(ptr)
|
|
15
|
+
Leptris::XML::FFI.leptris_xpath_compiled_free(ptr)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :expression
|
|
20
|
+
|
|
21
|
+
def self.compile(expression)
|
|
22
|
+
raw = Leptris::XML::FFI.leptris_xpath_compile(expression.to_s)
|
|
23
|
+
if raw.null?
|
|
24
|
+
raise Leptris::XML::XPathError,
|
|
25
|
+
"invalid expression: #{Leptris::XML::FFI.leptris_last_error}"
|
|
26
|
+
end
|
|
27
|
+
new(expression.to_s, CompiledHandle.new(raw))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(expression, handle)
|
|
31
|
+
@expression = expression
|
|
32
|
+
@handle = handle
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Evaluates against +doc_or_element+. An optional trailing hash of
|
|
36
|
+
# namespace bindings ("prefix" => uri) routes the evaluation through
|
|
37
|
+
# the namespace-bound path, matching Searchable#xpath semantics.
|
|
38
|
+
def eval(doc_or_element, ns = nil)
|
|
39
|
+
context = resolve_context(doc_or_element)
|
|
40
|
+
document = context.document
|
|
41
|
+
result_ptr =
|
|
42
|
+
if ns && !ns.empty?
|
|
43
|
+
flat = ns.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
|
|
44
|
+
buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
|
|
45
|
+
set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
|
|
46
|
+
buffer, flat.length / 2)
|
|
47
|
+
raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
|
|
48
|
+
begin
|
|
49
|
+
Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
|
|
50
|
+
@handle, document.c_ptr, context_ptr(context), set)
|
|
51
|
+
ensure
|
|
52
|
+
Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
Leptris::XML::FFI.leptris_xpath_compiled_eval(
|
|
56
|
+
@handle, document.c_ptr, context_ptr(context))
|
|
57
|
+
end
|
|
58
|
+
if result_ptr.null?
|
|
59
|
+
raise Leptris::XML::XPathError,
|
|
60
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
61
|
+
end
|
|
62
|
+
Leptris::XML::Searchable.wrap_xpath_result(document, result_ptr)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def resolve_context(doc_or_element)
|
|
68
|
+
case doc_or_element
|
|
69
|
+
when Leptris::XML::Document then doc_or_element
|
|
70
|
+
when Leptris::XML::Element then doc_or_element
|
|
71
|
+
else
|
|
72
|
+
raise ArgumentError,
|
|
73
|
+
"expected a Leptris::XML::Document or Element, got #{doc_or_element.class}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def context_ptr(context)
|
|
78
|
+
context.is_a?(Leptris::XML::Document) ? nil : context.c_ptr
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/leptris/xml.rb
CHANGED
|
@@ -21,11 +21,14 @@ module Leptris
|
|
|
21
21
|
autoload :CStringArray, "leptris/xml/c_string_array"
|
|
22
22
|
autoload :Serialization, "leptris/xml/serialization"
|
|
23
23
|
autoload :SAX, "leptris/xml/sax"
|
|
24
|
+
autoload :XPath, "leptris/xml/xpath"
|
|
25
|
+
autoload :Pull, "leptris/xml/pull"
|
|
26
|
+
autoload :Iterparse, "leptris/xml/iterparse"
|
|
24
27
|
|
|
25
28
|
# Nokogiri-style top-level entry points (Nokogiri::XML(...) /
|
|
26
29
|
# Nokogiri::XML.parse). Delegates to Document.parse.
|
|
27
|
-
def self.parse(xml_or_io, options: nil)
|
|
28
|
-
Document.parse(xml_or_io, options: options)
|
|
30
|
+
def self.parse(xml_or_io, options: nil, readonly: false)
|
|
31
|
+
Document.parse(xml_or_io, options: options, readonly: readonly)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def self.parse_file(path)
|
|
@@ -34,6 +37,7 @@ module Leptris
|
|
|
34
37
|
|
|
35
38
|
class Error < StandardError; end
|
|
36
39
|
class ParseError < Error; end
|
|
40
|
+
class ReadOnlyError < Error; end
|
|
37
41
|
class XPathError < Error; end
|
|
38
42
|
class UseAfterFreeError < Error; end
|
|
39
43
|
end
|
data/lib/libleptris.so
CHANGED
|
Binary file
|