leptris 1.0.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +529 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +405 -0
- data/Rakefile +7 -0
- data/TODO.impl/01-architecture.md +217 -0
- data/TODO.impl/02-ffi-declarations.md +236 -0
- data/TODO.impl/03-document-node-element-nodeset.md +382 -0
- data/TODO.impl/04-sax-parser.md +203 -0
- data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
- data/benchmark/README.md +168 -0
- data/benchmark/leptris_vs_nokogiri.rb +105 -0
- data/docs/ARCHITECTURE.adoc +559 -0
- data/docs/BUILD.md +395 -0
- data/docs/ERROR_MESSAGES.md +458 -0
- data/docs/FFI_ARCHITECTURE.md +439 -0
- data/docs/FUTURE_VISION.md +303 -0
- data/docs/GITHUB_ACTIONS.md +293 -0
- data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +459 -0
- data/docs/PERFORMANCE.adoc +668 -0
- data/docs/PERFORMANCE.md +448 -0
- data/docs/RELEASE_NOTES_v1.0.0.md +515 -0
- data/docs/XPATH_SPEC_COMPLIANCE.md +298 -0
- data/docs/completion/leptris.bash +86 -0
- data/docs/completion/leptris.zsh +74 -0
- data/docs/man/leptris-format.1 +227 -0
- data/docs/man/leptris-parse.1 +178 -0
- data/docs/man/leptris-xpath.1 +312 -0
- data/docs/man/leptris.1 +160 -0
- data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +217 -0
- data/docs/v0.9.0_RELEASE_SUMMARY.md +281 -0
- data/docs/v1.0.0_CONTINUATION_PLAN.md +172 -0
- data/docs/v1.0.0_CONTINUATION_PROMPT.md +382 -0
- data/docs/v1.0.0_SESSION_6_CONTINUATION.md +434 -0
- data/docs/v1.0.0_SESSION_6_PROMPT.md +231 -0
- data/docs/v1.0.0_STATUS_TRACKER.md +224 -0
- data/docs/v1.1.0_CONTINUATION_PLAN.md +299 -0
- data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +201 -0
- data/docs/v1.1.0_SESSION_3_PROMPT.md +223 -0
- data/docs/v1.1.0_STATUS_TRACKER.md +355 -0
- data/docs/xml-performance.adoc +115 -0
- data/docs/xpath-performance.adoc +379 -0
- data/leptris.gemspec +42 -0
- data/lib/leptris/version.rb +5 -0
- data/lib/leptris/xml/attr.rb +43 -0
- data/lib/leptris/xml/c14n.rb +23 -0
- data/lib/leptris/xml/cdata.rb +16 -0
- data/lib/leptris/xml/comment.rb +16 -0
- data/lib/leptris/xml/css_to_xpath.rb +177 -0
- data/lib/leptris/xml/doc_type.rb +54 -0
- data/lib/leptris/xml/document.rb +202 -0
- data/lib/leptris/xml/document_fragment.rb +42 -0
- data/lib/leptris/xml/element.rb +278 -0
- data/lib/leptris/xml/ffi.rb +420 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/node.rb +221 -0
- data/lib/leptris/xml/node_set.rb +143 -0
- data/lib/leptris/xml/parse_options.rb +19 -0
- data/lib/leptris/xml/processing_instruction.rb +26 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/parser.rb +148 -0
- data/lib/leptris/xml/sax.rb +12 -0
- data/lib/leptris/xml/searchable.rb +93 -0
- data/lib/leptris/xml/text.rb +16 -0
- data/lib/leptris/xml.rb +39 -0
- data/lib/leptris.rb +7 -0
- metadata +157 -0
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
@result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : @array.length
|
|
45
|
+
end
|
|
46
|
+
alias_method :size, :length
|
|
47
|
+
|
|
48
|
+
def empty?
|
|
49
|
+
length == 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def [](idx)
|
|
53
|
+
if @result_ptr
|
|
54
|
+
return nil if idx < 0 || idx >= length
|
|
55
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get(@result_ptr, idx)
|
|
56
|
+
return nil if ptr.null?
|
|
57
|
+
Leptris::XML::Node.wrap(ptr, @document)
|
|
58
|
+
else
|
|
59
|
+
@array[idx]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each
|
|
64
|
+
return enum_for(:each) unless block_given?
|
|
65
|
+
if @result_ptr
|
|
66
|
+
# Batch-fetch all node pointers in one FFI call (leptris_xpath_result_get_nodes,
|
|
67
|
+
# libleptris v0.11.4) and wrap each. Saves N-1 FFI calls vs the per-index
|
|
68
|
+
# leptris_xpath_result_get loop. Wrappers are still cached per-Document via
|
|
69
|
+
# Node.wrap, so a re-iteration of the same NodeSet hits the cache.
|
|
70
|
+
n = length
|
|
71
|
+
if n > 0
|
|
72
|
+
buf = ::FFI::MemoryPointer.new(:pointer, n)
|
|
73
|
+
begin
|
|
74
|
+
copied = Leptris::XML::FFI.leptris_xpath_result_get_nodes(@result_ptr, buf, n)
|
|
75
|
+
copied.times do |i|
|
|
76
|
+
ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
|
|
77
|
+
next if ptr.null?
|
|
78
|
+
yield Leptris::XML::Node.wrap(ptr, @document)
|
|
79
|
+
end
|
|
80
|
+
ensure
|
|
81
|
+
buf.free
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
@array.each { |n| yield n }
|
|
86
|
+
end
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_a
|
|
91
|
+
return @array if @array
|
|
92
|
+
return [] if @result_ptr.nil?
|
|
93
|
+
out = []
|
|
94
|
+
each { |n| out << n }
|
|
95
|
+
@array = out # memoize so subsequent to_a / each avoids re-fetch
|
|
96
|
+
out
|
|
97
|
+
end
|
|
98
|
+
alias_method :to_ary, :to_a
|
|
99
|
+
|
|
100
|
+
def first(n = nil)
|
|
101
|
+
return self[0] if n.nil?
|
|
102
|
+
n.times.map { |i| self[i] }.take_while { |x| !x.nil? }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def last
|
|
106
|
+
if @result_ptr
|
|
107
|
+
self[length - 1]
|
|
108
|
+
else
|
|
109
|
+
@array.last
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def inner_text
|
|
114
|
+
map(&:content).join
|
|
115
|
+
end
|
|
116
|
+
alias_method :text, :inner_text
|
|
117
|
+
|
|
118
|
+
def xpath(*paths)
|
|
119
|
+
handler, _ns, _vars = parse_search_args(paths)
|
|
120
|
+
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
121
|
+
expr = paths.join(" | ")
|
|
122
|
+
accumulated = Leptris::XML::NodeSet.new(@document)
|
|
123
|
+
each do |node|
|
|
124
|
+
next unless node.is_a?(Leptris::XML::Element)
|
|
125
|
+
result_ptr = Leptris::XML::FFI.leptris_xpath_eval(
|
|
126
|
+
@document.c_ptr, node.c_ptr, expr)
|
|
127
|
+
next if result_ptr.null?
|
|
128
|
+
sub = Leptris::XML::NodeSet.send(:from_result, @document, result_ptr)
|
|
129
|
+
accumulated = merge_node_sets(accumulated, sub)
|
|
130
|
+
end
|
|
131
|
+
accumulated
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def inspect
|
|
135
|
+
to_a.inspect
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def merge_node_sets(a, b)
|
|
141
|
+
Leptris::XML::NodeSet.new(@document, a.to_a + b.to_a)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::ParseOptions
|
|
4
|
+
DEFAULT_XML = 0
|
|
5
|
+
RECOVER = 1 << 0
|
|
6
|
+
NOERROR = 1 << 5
|
|
7
|
+
NOWARNING = 1 << 6
|
|
8
|
+
NOCDATA = 1 << 8
|
|
9
|
+
STRICT = 1 << 18
|
|
10
|
+
|
|
11
|
+
attr_reader :options
|
|
12
|
+
|
|
13
|
+
def initialize(options = DEFAULT_XML)
|
|
14
|
+
@options = options.to_i
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def strict?; !@options.zero?; end
|
|
18
|
+
def recover?; @options & RECOVER != 0; end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
4
|
+
def name
|
|
5
|
+
Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr)
|
|
6
|
+
end
|
|
7
|
+
alias_method :target, :name
|
|
8
|
+
|
|
9
|
+
def content
|
|
10
|
+
Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def target=(new_target)
|
|
14
|
+
status = Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s)
|
|
15
|
+
raise Leptris::XML::Error,
|
|
16
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
17
|
+
new_target
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def data=(new_data)
|
|
21
|
+
status = Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s)
|
|
22
|
+
raise Leptris::XML::Error,
|
|
23
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
24
|
+
new_data
|
|
25
|
+
end
|
|
26
|
+
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,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
class Leptris::XML::SAX::Parser
|
|
6
|
+
CHUNK_SIZE = 4096
|
|
7
|
+
private_constant :CHUNK_SIZE
|
|
8
|
+
|
|
9
|
+
attr_accessor :document, :encoding
|
|
10
|
+
|
|
11
|
+
def initialize(handler = Leptris::XML::SAX::Document.new, encoding = nil)
|
|
12
|
+
@document = handler
|
|
13
|
+
@encoding = encoding
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Parse a string, IO, or file path. Dispatches to parse_memory /
|
|
17
|
+
# parse_io / parse_file based on the argument type.
|
|
18
|
+
def parse(input)
|
|
19
|
+
case input
|
|
20
|
+
when String then parse_memory(input)
|
|
21
|
+
when ->(x) { x.respond_to?(:read) } then parse_io(input)
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "SAX parser expects a String or IO, got #{input.class}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse_memory(string)
|
|
28
|
+
string = string.dup.force_encoding("UTF-8")
|
|
29
|
+
handler_struct = build_handler_struct
|
|
30
|
+
rc = Leptris::XML::FFI.leptris_sax_parse(
|
|
31
|
+
string, string.bytesize, handler_struct.pointer, nil)
|
|
32
|
+
if rc != 0
|
|
33
|
+
raise Leptris::XML::ParseError,
|
|
34
|
+
"leptris_sax_parse failed (rc=#{rc})"
|
|
35
|
+
end
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_io(io)
|
|
40
|
+
handler_struct = build_handler_struct
|
|
41
|
+
parser_ptr = Leptris::XML::FFI.leptris_sax_parser_create(
|
|
42
|
+
handler_struct.pointer, nil)
|
|
43
|
+
if parser_ptr.null?
|
|
44
|
+
raise Leptris::XML::Error, "leptris_sax_parser_create failed"
|
|
45
|
+
end
|
|
46
|
+
begin
|
|
47
|
+
while (chunk = io.read(CHUNK_SIZE))
|
|
48
|
+
rc = Leptris::XML::FFI.leptris_sax_parser_feed(
|
|
49
|
+
parser_ptr, chunk, chunk.bytesize, 0)
|
|
50
|
+
if rc != 0
|
|
51
|
+
raise Leptris::XML::ParseError, "leptris_sax_parser_feed failed (rc=#{rc})"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
# Final flush
|
|
55
|
+
rc = Leptris::XML::FFI.leptris_sax_parser_feed(parser_ptr, "", 0, 1)
|
|
56
|
+
if rc != 0
|
|
57
|
+
raise Leptris::XML::ParseError, "leptris_sax_parser_feed (final) failed (rc=#{rc})"
|
|
58
|
+
end
|
|
59
|
+
ensure
|
|
60
|
+
Leptris::XML::FFI.leptris_sax_parser_free(parser_ptr)
|
|
61
|
+
end
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_file(path)
|
|
66
|
+
File.open(path, "r") { |f| parse_io(f) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Build a LeptrisSAXHandler struct populated with FFI::Function callbacks
|
|
72
|
+
# that dispatch to the Ruby handler. The struct (and its callbacks) are
|
|
73
|
+
# anchored against GC via the local variable for the duration of the
|
|
74
|
+
# synchronous parse call.
|
|
75
|
+
def build_handler_struct
|
|
76
|
+
s = Leptris::XML::FFI::SAXHandler.new
|
|
77
|
+
handler = @document # capture in closures
|
|
78
|
+
|
|
79
|
+
s[:start_document] = callback(:void, [:pointer]) do |_|
|
|
80
|
+
handler.start_document
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
s[:end_document] = callback(:void, [:pointer]) do |_|
|
|
84
|
+
handler.end_document
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
|
|
88
|
+
attrs = walk_attr_array(attrs_ptr)
|
|
89
|
+
handler.start_element(name, attrs)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
s[:end_element] = callback(:void, [:pointer, :string]) do |_, name|
|
|
93
|
+
handler.end_element(name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
|
|
97
|
+
handler.characters(text_ptr.read_bytes(len).force_encoding("UTF-8"))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
s[:comment] = callback(:void, [:pointer, :string]) do |_, comment|
|
|
101
|
+
handler.comment(comment)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata|
|
|
105
|
+
handler.cdata_block(cdata)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data|
|
|
109
|
+
handler.processing_instruction(target, data)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri|
|
|
113
|
+
handler.start_prefix_mapping(prefix, uri)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix|
|
|
117
|
+
handler.end_prefix_mapping(prefix)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
|
|
121
|
+
handler.error(msg, line, col)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def callback(return_type, params, blocking: true, &block)
|
|
128
|
+
::FFI::Function.new(return_type, params, blocking: blocking, &block)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The C `const char** attrs` is a NULL-terminated flat array of name/value
|
|
132
|
+
# pairs: [name1, value1, name2, value2, ..., NULL]. Walk into pairs.
|
|
133
|
+
def walk_attr_array(attrs_ptr)
|
|
134
|
+
return [] if attrs_ptr.null?
|
|
135
|
+
ptr_size = ::FFI.type_size(:pointer)
|
|
136
|
+
result = []
|
|
137
|
+
offset = 0
|
|
138
|
+
loop do
|
|
139
|
+
name_ptr = attrs_ptr.get_pointer(offset)
|
|
140
|
+
break if name_ptr.null?
|
|
141
|
+
value_ptr = attrs_ptr.get_pointer(offset + ptr_size)
|
|
142
|
+
break if value_ptr.null?
|
|
143
|
+
result << [name_ptr.read_string, value_ptr.read_string]
|
|
144
|
+
offset += 2 * ptr_size
|
|
145
|
+
end
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Leptris::XML::Searchable
|
|
4
|
+
def xpath(*paths)
|
|
5
|
+
handler, _ns, _vars = parse_search_args(paths)
|
|
6
|
+
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
7
|
+
expr = paths.join(" | ")
|
|
8
|
+
|
|
9
|
+
doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
|
|
10
|
+
context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
|
|
11
|
+
|
|
12
|
+
result_ptr = Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
|
|
13
|
+
if result_ptr.null?
|
|
14
|
+
raise Leptris::XML::XPathError,
|
|
15
|
+
Leptris::XML::FFI.leptris_status_string(Leptris::XML::FFI::LEPTRIS_ERROR_XPATH)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
wrap_xpath_result(result_ptr)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def at_xpath(*paths)
|
|
22
|
+
result = xpath(*paths)
|
|
23
|
+
result.is_a?(Leptris::XML::NodeSet) ? result.first : result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def search(*args)
|
|
27
|
+
paths = args.first.is_a?(Array) ? args.first : [args.first]
|
|
28
|
+
paths.map(&:to_s).all? { |p| looks_like_xpath?(p) } ? xpath(*paths) : css(*paths)
|
|
29
|
+
end
|
|
30
|
+
alias_method :/, :search
|
|
31
|
+
|
|
32
|
+
def at(*args)
|
|
33
|
+
result = search(*args)
|
|
34
|
+
result.is_a?(Leptris::XML::NodeSet) ? result.first : result
|
|
35
|
+
end
|
|
36
|
+
alias_method :%, :at
|
|
37
|
+
|
|
38
|
+
def css(*args)
|
|
39
|
+
handler, ns, _ = parse_search_args(args)
|
|
40
|
+
raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty?
|
|
41
|
+
raise ArgumentError, "custom CSS handlers not supported" if handler
|
|
42
|
+
expr = args.map { |r| Leptris::XML::CssToXPath.convert(r) }.join(" | ")
|
|
43
|
+
xpath(expr)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def at_css(*args)
|
|
47
|
+
result = css(*args)
|
|
48
|
+
result.is_a?(Leptris::XML::NodeSet) ? result.first : result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
def parse_search_args(args)
|
|
54
|
+
handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) }
|
|
55
|
+
args = args - [handler] if handler
|
|
56
|
+
hashes = []
|
|
57
|
+
while args.last.is_a?(Hash) || args.last.nil?
|
|
58
|
+
hashes << args.pop
|
|
59
|
+
break if args.empty?
|
|
60
|
+
end
|
|
61
|
+
ns, vars = hashes.reverse
|
|
62
|
+
[handler, ns, vars]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def looks_like_xpath?(str)
|
|
66
|
+
%r{\A(\./|/|\.\.|\.)}.match?(str)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def wrap_xpath_result(result_ptr)
|
|
70
|
+
type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
|
|
71
|
+
case type
|
|
72
|
+
when Leptris::XML::FFI::XPATH_NODESET
|
|
73
|
+
Leptris::XML::NodeSet.send(:from_result, document, result_ptr)
|
|
74
|
+
when Leptris::XML::FFI::XPATH_BOOLEAN
|
|
75
|
+
v = Leptris::XML::FFI.leptris_xpath_result_boolean(result_ptr) != 0
|
|
76
|
+
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
77
|
+
v
|
|
78
|
+
when Leptris::XML::FFI::XPATH_NUMBER
|
|
79
|
+
v = Leptris::XML::FFI.leptris_xpath_result_number(result_ptr)
|
|
80
|
+
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
81
|
+
v
|
|
82
|
+
when Leptris::XML::FFI::XPATH_STRING
|
|
83
|
+
str_ptr = Leptris::XML::FFI.leptris_xpath_result_string(result_ptr)
|
|
84
|
+
v = str_ptr.null? ? "" : str_ptr.read_string
|
|
85
|
+
Leptris::XML::FFI.leptris_free_string(str_ptr) unless str_ptr.null?
|
|
86
|
+
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
87
|
+
v
|
|
88
|
+
else
|
|
89
|
+
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
90
|
+
raise Leptris::XML::XPathError, "unknown xpath result type #{type}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::Text < Leptris::XML::Node
|
|
4
|
+
def name; "text"; end
|
|
5
|
+
|
|
6
|
+
def content
|
|
7
|
+
Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content=(new_content)
|
|
11
|
+
status = Leptris::XML::FFI.leptris_text_node_set_content(@c_ptr, new_content.to_s)
|
|
12
|
+
raise Leptris::XML::Error,
|
|
13
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
14
|
+
new_content
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/leptris/xml.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Leptris
|
|
4
|
+
module XML
|
|
5
|
+
autoload :FFI, "leptris/xml/ffi"
|
|
6
|
+
autoload :Node, "leptris/xml/node"
|
|
7
|
+
autoload :Element, "leptris/xml/element"
|
|
8
|
+
autoload :Text, "leptris/xml/text"
|
|
9
|
+
autoload :Comment, "leptris/xml/comment"
|
|
10
|
+
autoload :CDATA, "leptris/xml/cdata"
|
|
11
|
+
autoload :ProcessingInstruction, "leptris/xml/processing_instruction"
|
|
12
|
+
autoload :Attr, "leptris/xml/attr"
|
|
13
|
+
autoload :Namespace, "leptris/xml/namespace"
|
|
14
|
+
autoload :Document, "leptris/xml/document"
|
|
15
|
+
autoload :DocumentFragment, "leptris/xml/document_fragment"
|
|
16
|
+
autoload :DocType, "leptris/xml/doc_type"
|
|
17
|
+
autoload :NodeSet, "leptris/xml/node_set"
|
|
18
|
+
autoload :Searchable, "leptris/xml/searchable"
|
|
19
|
+
autoload :ParseOptions, "leptris/xml/parse_options"
|
|
20
|
+
autoload :CssToXPath, "leptris/xml/css_to_xpath"
|
|
21
|
+
autoload :SAX, "leptris/xml/sax"
|
|
22
|
+
require "leptris/xml/c14n" # module-level helper, eager load
|
|
23
|
+
|
|
24
|
+
# Nokogiri-style top-level entry points (Nokogiri::XML(...) /
|
|
25
|
+
# Nokogiri::XML.parse). Delegates to Document.parse.
|
|
26
|
+
def self.parse(xml_or_io)
|
|
27
|
+
Document.parse(xml_or_io)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.parse_file(path)
|
|
31
|
+
Document.parse_file(path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Error < StandardError; end
|
|
35
|
+
class ParseError < Error; end
|
|
36
|
+
class XPathError < Error; end
|
|
37
|
+
class UseAfterFreeError < Error; end
|
|
38
|
+
end
|
|
39
|
+
end
|