leptris 1.4.0 → 1.6.0
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 +42 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +18 -0
- data/lib/leptris/xml/ffi.rb +67 -0
- data/lib/leptris/xml/iterparse.rb +57 -0
- data/lib/leptris/xml/pull.rb +123 -0
- data/lib/leptris/xml/searchable.rb +5 -2
- data/lib/leptris/xml/xpath.rb +80 -0
- data/lib/leptris/xml.rb +3 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28f24417196438a9a1aef17c3e1f648b7d85048659268a332341c36663a4a16a
|
|
4
|
+
data.tar.gz: 542fa2497b09a734f2cd65ca3eb398c90d24841bc4ed70577e7ed177e323f521
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d46d2a265f3c07326fda5e152f338373772b9f804f5c5c7d8174dcb78dff3f10e5a875972b5635ab94e2309001552e85f0a45c26e14687c713d8647cdd32815c
|
|
7
|
+
data.tar.gz: a7ae147afbd49cf1afe4eebfd6f9e2ba4dea2e682a2da7cbd3e7b892aaf5bc3150eeffca6bdecd840a166499d17eefd6debf4f4df3e0fe59d34cdfe0613da84f
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.0] - 2026-08-24
|
|
9
|
+
|
|
10
|
+
Lockstep with libleptris 1.6.0 — the moxml-adapter blockers fixed:
|
|
11
|
+
same-parent node moves no longer corrupt the sibling chain (#518),
|
|
12
|
+
detached PI/comment/CDATA mutation works on rootless documents
|
|
13
|
+
(#519); union nodesets keep attribute identity (#514); document-level
|
|
14
|
+
processing instructions gained a public API (#526).
|
|
15
|
+
|
|
16
|
+
## [1.5.0] - 2026-08-24
|
|
17
|
+
|
|
18
|
+
Lockstep with libleptris 1.5.0 — the engine pins the TODO.engine
|
|
19
|
+
release: file-backed pull/iterparse streaming (bounded memory off
|
|
20
|
+
disk), compiled XPath ns/vars contexts, Rust crate publish workflow.
|
|
21
|
+
|
|
8
22
|
## [1.4.0] - 2026-08-23
|
|
9
23
|
|
|
10
24
|
Lockstep with libleptris 1.4.0 — engine pins the TODO.bindings
|
|
@@ -12,6 +26,34 @@ release: pull (StAX) API, bounded iterparse, compiled XPath
|
|
|
12
26
|
expressions, per-parse options, truthful serialization encoding
|
|
13
27
|
declarations. Binding-side adoption of the new APIs follows.
|
|
14
28
|
|
|
29
|
+
## [1.6.0] - 2026-08-24
|
|
30
|
+
|
|
31
|
+
Lockstep with libleptris 1.6.0 (v1.4/v1.5 were fix releases with no
|
|
32
|
+
public API changes).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`Leptris::XML::XPath` — compiled expressions** (parse once,
|
|
37
|
+
evaluate many): `XPath.compile("//item[@qty > 3]")` returns a
|
|
38
|
+
reusable handle (GC-managed) whose `#eval(doc_or_element[, ns])`
|
|
39
|
+
shares result-wrapping semantics with `Searchable#xpath`,
|
|
40
|
+
including namespace bindings.
|
|
41
|
+
- **`Leptris::XML::Pull` — StAX-style pull parsing**:
|
|
42
|
+
`Pull.parse(xml) { |event| }` streams start_element (with captured
|
|
43
|
+
attributes), end_element, text, comment, cdata, pi, end_document,
|
|
44
|
+
and error events; `Pull.parse_file` streams from disk.
|
|
45
|
+
- **`Leptris::XML::Iterparse` — incremental tree iteration**: yields
|
|
46
|
+
each completed top-level child element; the previous subtree is
|
|
47
|
+
released as the next is produced (memory bounded by the largest
|
|
48
|
+
subtree, not the document). Yielded elements have no parent
|
|
49
|
+
Document; upstream v1 limitation: namespace prefixes are not
|
|
50
|
+
re-resolved.
|
|
51
|
+
- **Document-level processing instructions**: `Document#processing_instructions`
|
|
52
|
+
(array of [target, data]) and `Document#add_pi(target, data)` —
|
|
53
|
+
document PIs are not tree nodes, per the C contract.
|
|
54
|
+
- `leptris_parse_string_ex` bound (options-struct parse; the flags
|
|
55
|
+
path remains the default).
|
|
56
|
+
|
|
15
57
|
## [1.3.0] - 2026-08-23
|
|
16
58
|
|
|
17
59
|
Lockstep with libleptris 1.3.0 — takes full advantage of the
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
# Pin for `rake compile` and the platform-gem builds. Keep in lockstep
|
|
9
9
|
# with .github/workflows/build.yml (which calls `rake compile`) and the
|
|
10
10
|
# CHANGELOG when libleptris releases.
|
|
11
|
-
LIBLEPTRIS_VERSION = "1.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.6.0"
|
|
12
12
|
|
|
13
13
|
CMAKE_FLAGS = %w[
|
|
14
14
|
-DCMAKE_BUILD_TYPE=Release
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -234,6 +234,24 @@ class Leptris::XML::Document
|
|
|
234
234
|
self
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
+
# Document-level processing instructions (not tree nodes):
|
|
238
|
+
# an array of [target, data] pairs in document order.
|
|
239
|
+
def processing_instructions
|
|
240
|
+
count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
|
|
241
|
+
count.times.map do |i|
|
|
242
|
+
[Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
|
|
243
|
+
Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Append a document-level processing instruction. Returns self.
|
|
248
|
+
def add_pi(target, data = "")
|
|
249
|
+
witness = Leptris::XML::FFI.leptris_document_add_pi(
|
|
250
|
+
@c_ptr, target.to_s, data.to_s)
|
|
251
|
+
raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
|
|
252
|
+
self
|
|
253
|
+
end
|
|
254
|
+
|
|
237
255
|
# The most recent error recorded against this document, or nil.
|
|
238
256
|
def last_error
|
|
239
257
|
msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
|
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,
|
|
@@ -385,6 +402,20 @@ module Leptris
|
|
|
385
402
|
attach_function :leptris_xpath_eval_ns,
|
|
386
403
|
[:leptris_document, :leptris_element, :string, :leptris_xpath_ns_set],
|
|
387
404
|
:leptris_xpath_result
|
|
405
|
+
# Compiled expressions (v1.6.0): parse once, evaluate many.
|
|
406
|
+
attach_function :leptris_xpath_compile,
|
|
407
|
+
[:string], :leptris_xpath_compiled
|
|
408
|
+
attach_function :leptris_xpath_compiled_eval,
|
|
409
|
+
[:leptris_xpath_compiled, :leptris_document, :leptris_element],
|
|
410
|
+
:leptris_xpath_result
|
|
411
|
+
attach_function :leptris_xpath_compiled_eval_ns,
|
|
412
|
+
[:leptris_xpath_compiled, :leptris_document, :leptris_element,
|
|
413
|
+
:leptris_xpath_ns_set], :leptris_xpath_result
|
|
414
|
+
attach_function :leptris_xpath_compiled_eval_vars,
|
|
415
|
+
[:leptris_xpath_compiled, :leptris_document, :leptris_element,
|
|
416
|
+
:leptris_xpath_var_set], :leptris_xpath_result
|
|
417
|
+
attach_function :leptris_xpath_compiled_free,
|
|
418
|
+
[:leptris_xpath_compiled], :void
|
|
388
419
|
|
|
389
420
|
attach_function :leptris_sax_parse,
|
|
390
421
|
[:string, :size_t, :pointer, :pointer], :int
|
|
@@ -397,6 +428,33 @@ module Leptris
|
|
|
397
428
|
attach_function :leptris_sax_parser_set_streaming,
|
|
398
429
|
[:leptris_sax_parser, :int], :int
|
|
399
430
|
|
|
431
|
+
# Pull parsing (v1.6.0): StAX-style cursor over a document.
|
|
432
|
+
attach_function :leptris_pull_new,
|
|
433
|
+
[:string, :size_t], :leptris_pull_parser
|
|
434
|
+
attach_function :leptris_pull_new_file,
|
|
435
|
+
[:string], :leptris_pull_parser
|
|
436
|
+
attach_function :leptris_pull_next,
|
|
437
|
+
[:leptris_pull_parser], :pointer
|
|
438
|
+
attach_function :leptris_pull_attr_count,
|
|
439
|
+
[:leptris_pull_parser], :size_t
|
|
440
|
+
attach_function :leptris_pull_attr_name,
|
|
441
|
+
[:leptris_pull_parser, :size_t], :string
|
|
442
|
+
attach_function :leptris_pull_attr_value,
|
|
443
|
+
[:leptris_pull_parser, :size_t], :string
|
|
444
|
+
attach_function :leptris_pull_free,
|
|
445
|
+
[:leptris_pull_parser], :void
|
|
446
|
+
|
|
447
|
+
# Iterparse (v1.6.0): yields each completed top-level child
|
|
448
|
+
# element; the previous subtree is released on each next call.
|
|
449
|
+
attach_function :leptris_iterparse_new,
|
|
450
|
+
[:string, :size_t], :leptris_iterparse
|
|
451
|
+
attach_function :leptris_iterparse_new_file,
|
|
452
|
+
[:string], :leptris_iterparse
|
|
453
|
+
attach_function :leptris_iterparse_next,
|
|
454
|
+
[:leptris_iterparse], :leptris_element
|
|
455
|
+
attach_function :leptris_iterparse_free,
|
|
456
|
+
[:leptris_iterparse], :void
|
|
457
|
+
|
|
400
458
|
attach_function :leptris_document_serialize,
|
|
401
459
|
[:leptris_document, :pointer], :pointer
|
|
402
460
|
# Legacy simple serializer, declared retroactively in v1.1.0.
|
|
@@ -491,6 +549,15 @@ module Leptris
|
|
|
491
549
|
TRAVERSE_PRE_ORDER = 0
|
|
492
550
|
TRAVERSE_POST_ORDER = 1
|
|
493
551
|
|
|
552
|
+
PULL_START_ELEMENT = 0
|
|
553
|
+
PULL_END_ELEMENT = 1
|
|
554
|
+
PULL_TEXT = 2
|
|
555
|
+
PULL_COMMENT = 3
|
|
556
|
+
PULL_CDATA = 4
|
|
557
|
+
PULL_PI = 5
|
|
558
|
+
PULL_END_DOCUMENT = 6
|
|
559
|
+
PULL_ERROR = 7
|
|
560
|
+
|
|
494
561
|
LEPTRIS_PARSE_DEFAULT = 0
|
|
495
562
|
LEPTRIS_PARSE_DROP_WS_TEXT = 1
|
|
496
563
|
|
|
@@ -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
|
|
@@ -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 TaurusXPathResult 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
|
|
@@ -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,6 +21,9 @@ 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.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -121,17 +121,20 @@ files:
|
|
|
121
121
|
- lib/leptris/xml/document_fragment.rb
|
|
122
122
|
- lib/leptris/xml/element.rb
|
|
123
123
|
- lib/leptris/xml/ffi.rb
|
|
124
|
+
- lib/leptris/xml/iterparse.rb
|
|
124
125
|
- lib/leptris/xml/namespace.rb
|
|
125
126
|
- lib/leptris/xml/node.rb
|
|
126
127
|
- lib/leptris/xml/node_set.rb
|
|
127
128
|
- lib/leptris/xml/parse_options.rb
|
|
128
129
|
- lib/leptris/xml/processing_instruction.rb
|
|
130
|
+
- lib/leptris/xml/pull.rb
|
|
129
131
|
- lib/leptris/xml/sax.rb
|
|
130
132
|
- lib/leptris/xml/sax/document.rb
|
|
131
133
|
- lib/leptris/xml/sax/parser.rb
|
|
132
134
|
- lib/leptris/xml/searchable.rb
|
|
133
135
|
- lib/leptris/xml/serialization.rb
|
|
134
136
|
- lib/leptris/xml/text.rb
|
|
137
|
+
- lib/leptris/xml/xpath.rb
|
|
135
138
|
homepage: https://github.com/leptris/leptris-ruby
|
|
136
139
|
licenses:
|
|
137
140
|
- MIT
|