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,236 @@
|
|
|
1
|
+
# TODO 2 — FFI declarations: complete libleptris v0.4.2 public API
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Create `lib/leptris/xml/ffi.rb` that attaches to EVERY public function
|
|
6
|
+
in libleptris v0.4.2 via the `ffi` gem. This is the single source of
|
|
7
|
+
truth for the C ↔ Ruby boundary.
|
|
8
|
+
|
|
9
|
+
## Library loading
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
module Leptris
|
|
13
|
+
module XML
|
|
14
|
+
module FFI
|
|
15
|
+
extend ::FFI
|
|
16
|
+
|
|
17
|
+
ffi_lib [
|
|
18
|
+
ENV['LEPTRIS_LIB_PATH'],
|
|
19
|
+
'leptris',
|
|
20
|
+
'/usr/local/lib/libleptris.dylib',
|
|
21
|
+
'/usr/local/lib/libleptris.so',
|
|
22
|
+
File.expand_path('../../../build/src/libleptris.dylib', __dir__),
|
|
23
|
+
File.expand_path('../../../build/src/libleptris.so', __dir__),
|
|
24
|
+
].compact
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Opaque type declarations
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
typedef :pointer, :document
|
|
34
|
+
typedef :pointer, :element
|
|
35
|
+
typedef :pointer, :node_ref
|
|
36
|
+
typedef :pointer, :xpath_result
|
|
37
|
+
typedef :pointer, :sax_parser
|
|
38
|
+
typedef :pointer, :attribute
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Complete function list (attach all of these)
|
|
42
|
+
|
|
43
|
+
Source: `src/include/leptris/types.h`, `src/include/leptris.h`, and
|
|
44
|
+
`src/include/leptris/*.h`.
|
|
45
|
+
|
|
46
|
+
### Version
|
|
47
|
+
```ruby
|
|
48
|
+
attach_function :leptris_version, [], :string
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Document lifecycle
|
|
52
|
+
```ruby
|
|
53
|
+
attach_function :leptris_parse_string, [:string, :size_t, :pointer], :document
|
|
54
|
+
attach_function :leptris_document_free, [:document], :void
|
|
55
|
+
attach_function :leptris_document_root, [:document], :element
|
|
56
|
+
attach_function :leptris_document_serialize, [:document, :pointer], :pointer
|
|
57
|
+
attach_function :leptris_document_set_strict, [:document, :int], :void
|
|
58
|
+
attach_function :leptris_xinclude_process, [:document, :string], :int
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Node access
|
|
62
|
+
```ruby
|
|
63
|
+
attach_function :leptris_node_get_type, [:node_ref], :int
|
|
64
|
+
attach_function :leptris_node_first_child, [:node_ref], :node_ref
|
|
65
|
+
attach_function :leptris_node_next_sibling, [:node_ref], :node_ref
|
|
66
|
+
attach_function :leptris_node_previous_sibling, [:node_ref], :node_ref
|
|
67
|
+
attach_function :leptris_node_child_count, [:node_ref], :size_t
|
|
68
|
+
attach_function :leptris_node_as_element, [:node_ref], :element
|
|
69
|
+
attach_function :leptris_element_as_node, [:element], :node_ref
|
|
70
|
+
attach_function :leptris_element_first_child_any, [:element], :element
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Element queries
|
|
74
|
+
```ruby
|
|
75
|
+
attach_function :leptris_element_name, [:element], :string
|
|
76
|
+
attach_function :leptris_element_text, [:element], :string
|
|
77
|
+
attach_function :leptris_element_attribute, [:element, :string, :string], :string
|
|
78
|
+
attach_function :leptris_element_attribute_count, [:element], :size_t
|
|
79
|
+
attach_function :leptris_element_first_attribute, [:element], :pointer
|
|
80
|
+
attach_function :leptris_element_parent, [:element], :element
|
|
81
|
+
attach_function :leptris_element_next_sibling_any, [:element], :element
|
|
82
|
+
attach_function :leptris_element_get_namespace_uri, [:element], :string
|
|
83
|
+
attach_function :leptris_element_get_prefix, [:element], :string
|
|
84
|
+
attach_function :leptris_element_get_name, [:element], :string
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Element mutation
|
|
88
|
+
```ruby
|
|
89
|
+
attach_function :leptris_element_set_name, [:element, :string], :void
|
|
90
|
+
attach_function :leptris_element_set_attribute, [:element, :string, :string], :void
|
|
91
|
+
attach_function :leptris_element_remove_attribute, [:element, :string], :void
|
|
92
|
+
attach_function :leptris_element_append_child, [:element, :element], :int
|
|
93
|
+
attach_function :leptris_element_create_child, [:element, :string], :element
|
|
94
|
+
attach_function :leptris_element_set_text, [:element, :string], :void
|
|
95
|
+
attach_function :leptris_element_remove_child, [:element, :element], :void
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Element creation
|
|
99
|
+
```ruby
|
|
100
|
+
attach_function :leptris_element_create, [:string], :element
|
|
101
|
+
attach_function :leptris_text_node_create, [:string], :element
|
|
102
|
+
attach_function :leptris_comment_node_create, [:string], :element
|
|
103
|
+
attach_function :leptris_cdata_node_create, [:string], :element
|
|
104
|
+
attach_function :leptris_pi_node_create, [:string, :string], :element
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Text / Comment / CDATA / PI access
|
|
108
|
+
```ruby
|
|
109
|
+
attach_function :leptris_text_node_get_content, [:node_ref], :string
|
|
110
|
+
attach_function :leptris_comment_node_get_content, [:node_ref], :string
|
|
111
|
+
attach_function :leptris_cdata_node_get_content, [:node_ref], :string
|
|
112
|
+
attach_function :leptris_pi_node_get_target, [:node_ref], :string
|
|
113
|
+
attach_function :leptris_pi_node_get_data, [:node_ref], :string
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### XPath
|
|
117
|
+
```ruby
|
|
118
|
+
attach_function :leptris_xpath_eval,
|
|
119
|
+
[:document, :element, :string], :xpath_result
|
|
120
|
+
attach_function :leptris_xpath_eval_with_vars,
|
|
121
|
+
[:document, :string, :pointer], :xpath_result
|
|
122
|
+
attach_function :leptris_xpath_result_type, [:xpath_result], :int
|
|
123
|
+
attach_function :leptris_xpath_result_count, [:xpath_result], :size_t
|
|
124
|
+
attach_function :leptris_xpath_result_get, [:xpath_result, :size_t], :element
|
|
125
|
+
attach_function :leptris_xpath_result_boolean, [:xpath_result], :int
|
|
126
|
+
attach_function :leptris_xpath_result_number, [:xpath_result], :double
|
|
127
|
+
attach_function :leptris_xpath_result_string, [:xpath_result], :pointer
|
|
128
|
+
attach_function :leptris_xpath_result_free, [:xpath_result], :void
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### XPath variables
|
|
132
|
+
```ruby
|
|
133
|
+
attach_function :leptris_xpath_variable_set_new, [], :pointer
|
|
134
|
+
attach_function :leptris_xpath_variable_set_free, [:pointer], :void
|
|
135
|
+
attach_function :leptris_xpath_variable_set_boolean, [:pointer, :string, :int], :int
|
|
136
|
+
attach_function :leptris_xpath_variable_set_number, [:pointer, :string, :double], :int
|
|
137
|
+
attach_function :leptris_xpath_variable_set_string, [:pointer, :string, :string], :int
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### SAX
|
|
141
|
+
```ruby
|
|
142
|
+
# LeptrisSAXHandler is a struct of function pointers. Use FFI::Struct.
|
|
143
|
+
class SAXHandler < ::FFI::Struct
|
|
144
|
+
layout \
|
|
145
|
+
:start_document, :pointer,
|
|
146
|
+
:end_document, :pointer,
|
|
147
|
+
:start_element, :pointer,
|
|
148
|
+
:end_element, :pointer,
|
|
149
|
+
:characters, :pointer,
|
|
150
|
+
:comment, :pointer,
|
|
151
|
+
:cdata, :pointer,
|
|
152
|
+
:processing_instruction, :pointer,
|
|
153
|
+
:start_prefix_mapping, :pointer,
|
|
154
|
+
:end_prefix_mapping, :pointer,
|
|
155
|
+
:error, :pointer
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
attach_function :leptris_sax_parse,
|
|
159
|
+
[:string, :size_t, SAXHandler.by_pointer, :pointer], :int
|
|
160
|
+
attach_function :leptris_sax_parser_create,
|
|
161
|
+
[SAXHandler.by_pointer, :pointer], :sax_parser
|
|
162
|
+
attach_function :leptris_sax_parser_feed,
|
|
163
|
+
[:sax_parser, :string, :size_t, :int], :int
|
|
164
|
+
attach_function :leptris_sax_parser_free, [:sax_parser], :void
|
|
165
|
+
attach_function :leptris_sax_parser_set_streaming,
|
|
166
|
+
[:sax_parser, :int], :int
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Serialization
|
|
170
|
+
```ruby
|
|
171
|
+
# LeptrisSerializeOptions struct
|
|
172
|
+
class SerializeOptions < ::FFI::Struct
|
|
173
|
+
layout \
|
|
174
|
+
:indent, :int,
|
|
175
|
+
:xml_declaration, :int,
|
|
176
|
+
:no_empty_tags, :int,
|
|
177
|
+
:preserve_whitespace, :int
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
attach_function :leptris_serialize_document,
|
|
181
|
+
[:document, :pointer], :pointer
|
|
182
|
+
attach_function :leptris_c14n_canonicalize,
|
|
183
|
+
[:document, :int, :int], :pointer
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Memory
|
|
187
|
+
```ruby
|
|
188
|
+
attach_function :leptris_free_string, [:pointer], :void
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Status codes
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
LEPTRIS_OK = 0
|
|
195
|
+
LEPTRIS_ERROR_MEMORY = -1
|
|
196
|
+
LEPTRIS_ERROR_PARSE = -2
|
|
197
|
+
LEPTRIS_ERROR_XPATH = -3
|
|
198
|
+
LEPTRIS_ERROR_NULL_ARG = -4
|
|
199
|
+
LEPTRIS_ERROR_INVALID_ARG = -5
|
|
200
|
+
LEPTRIS_ERROR_NOT_FOUND = -6
|
|
201
|
+
LEPTRIS_ERROR_IO = -7
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Node type constants
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
NODE_ELEMENT = 0
|
|
208
|
+
NODE_ATTRIBUTE = 1
|
|
209
|
+
NODE_TEXT = 2
|
|
210
|
+
NODE_COMMENT = 3
|
|
211
|
+
NODE_CDATA = 4
|
|
212
|
+
NODE_PI = 5
|
|
213
|
+
NODE_DOCTYPE = 6
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## XPath result type constants
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
XPATH_NODESET = 0
|
|
220
|
+
XPATH_BOOLEAN = 1
|
|
221
|
+
XPATH_NUMBER = 2
|
|
222
|
+
XPATH_STRING = 3
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Notes
|
|
226
|
+
|
|
227
|
+
- Use `Blocking: true` for SAX callbacks (FFI::Function).
|
|
228
|
+
- Use `AutoPointer` for document and xpath_result to get automatic
|
|
229
|
+
cleanup. But ALSO provide explicit `#free` methods since GC timing
|
|
230
|
+
is non-deterministic.
|
|
231
|
+
- The `leptris_element_*` functions that return `:string` return
|
|
232
|
+
document-owned strings (valid until `leptris_document_free`).
|
|
233
|
+
Ruby copies them automatically on FFI return — safe.
|
|
234
|
+
- Functions that return `:pointer` for strings (like
|
|
235
|
+
`leptris_xpath_result_string`, `leptris_serialize_document`) return
|
|
236
|
+
heap-owned strings that the caller must free via `leptris_free_string`.
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# TODO 3 — Document, Node, Element, NodeSet, Searchable implementation
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the core Nokogiri-compatible Ruby classes backed by FFI
|
|
6
|
+
calls to libleptris. Each Ruby method = one C function call.
|
|
7
|
+
|
|
8
|
+
## File layout
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
lib/leptris.rb # autoload + version
|
|
12
|
+
lib/leptris/xml.rb # XML module + parse entry points
|
|
13
|
+
lib/leptris/xml/ffi.rb # FFI declarations (TODO 2)
|
|
14
|
+
lib/leptris/xml/document.rb # Document class
|
|
15
|
+
lib/leptris/xml/node.rb # Node base class
|
|
16
|
+
lib/leptris/xml/element.rb # Element < Node
|
|
17
|
+
lib/leptris/xml/text.rb # Text < Node
|
|
18
|
+
lib/leptris/xml/comment.rb # Comment < Node
|
|
19
|
+
lib/leptris/xml/cdata.rb # CDATA < Node
|
|
20
|
+
lib/leptris/xml/processing_instruction.rb # ProcessingInstruction < Node
|
|
21
|
+
lib/leptris/xml/attr.rb # Attr class
|
|
22
|
+
lib/leptris/xml/node_set.rb # NodeSet class
|
|
23
|
+
lib/leptris/xml/searchable.rb # Searchable mixin (xpath/css/search)
|
|
24
|
+
lib/leptris/xml/parse_options.rb # ParseOptions class
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Autoload pattern
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# lib/leptris.rb
|
|
31
|
+
module Leptris
|
|
32
|
+
autoload :XML, 'leptris/xml'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# lib/leptris/xml.rb
|
|
36
|
+
module Leptris
|
|
37
|
+
module XML
|
|
38
|
+
autoload :FFI, 'leptris/xml/ffi'
|
|
39
|
+
autoload :Document, 'leptris/xml/document'
|
|
40
|
+
autoload :Node, 'leptris/xml/node'
|
|
41
|
+
autoload :Element, 'leptris/xml/element'
|
|
42
|
+
autoload :Text, 'leptris/xml/text'
|
|
43
|
+
autoload :Comment, 'leptris/xml/comment'
|
|
44
|
+
autoload :CDATA, 'leptris/xml/cdata'
|
|
45
|
+
autoload :ProcessingInstruction, 'leptris/xml/processing_instruction'
|
|
46
|
+
autoload :Attr, 'leptris/xml/attr'
|
|
47
|
+
autoload :NodeSet, 'leptris/xml/node_set'
|
|
48
|
+
autoload :Searchable, 'leptris/xml/searchable'
|
|
49
|
+
autoload :ParseOptions, 'leptris/xml/parse_options'
|
|
50
|
+
|
|
51
|
+
def self.parse(string_or_io, options = nil)
|
|
52
|
+
xml = string_or_io.respond_to?(:read) ? string_or_io.read : string_or_io
|
|
53
|
+
status = ::FFI::MemoryPointer.new(:int)
|
|
54
|
+
ptr = FFI.leptris_parse_string(xml, xml.bytesize, status)
|
|
55
|
+
raise ParseError, "leptris_parse_string failed (status=#{status.read_int})" if ptr.nil? || ptr.null?
|
|
56
|
+
Document.wrap(ptr)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Document
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class Leptris::XML::Document < Leptris::XML::Node
|
|
66
|
+
def self.wrap(c_ptr)
|
|
67
|
+
doc = allocate
|
|
68
|
+
doc.instance_variable_set(:@c_ptr, c_ptr)
|
|
69
|
+
doc
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def root
|
|
73
|
+
ptr = FFI.leptris_document_root(@c_ptr)
|
|
74
|
+
return nil if ptr.nil? || ptr.null?
|
|
75
|
+
Leptris::XML::Element.wrap(ptr, self)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def xpath(expr)
|
|
79
|
+
Leptris::XML::XPath.evaluate(@c_ptr, nil, expr)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def at_xpath(expr)
|
|
83
|
+
result = xpath(expr)
|
|
84
|
+
result.is_a?(NodeSet) ? result.first : result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_xml(options = {})
|
|
88
|
+
opts = SerializeOptions.new
|
|
89
|
+
opts[:indent] = options[:indent] || 0
|
|
90
|
+
opts[:xml_declaration] = options[:no_decl] ? 0 : 1
|
|
91
|
+
ptr = FFI.leptris_serialize_document(@c_ptr, opts.pointer)
|
|
92
|
+
return '' if ptr.nil? || ptr.null?
|
|
93
|
+
str = ptr.read_string
|
|
94
|
+
FFI.leptris_free_string(ptr)
|
|
95
|
+
str
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def free
|
|
99
|
+
return unless @c_ptr
|
|
100
|
+
FFI.leptris_document_free(@c_ptr)
|
|
101
|
+
@c_ptr = nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Node (base class)
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Leptris::XML::Node
|
|
110
|
+
attr_reader :c_ptr, :document
|
|
111
|
+
|
|
112
|
+
def initialize(c_ptr, document)
|
|
113
|
+
@c_ptr = c_ptr
|
|
114
|
+
@document = document
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.wrap(c_ptr, document)
|
|
118
|
+
type = FFI.leptris_node_get_type(c_ptr)
|
|
119
|
+
case type
|
|
120
|
+
when NODE_ELEMENT then Leptris::XML::Element.new(c_ptr, document)
|
|
121
|
+
when NODE_TEXT then Leptris::XML::Text.new(c_ptr, document)
|
|
122
|
+
when NODE_COMMENT then Leptris::XML::Comment.new(c_ptr, document)
|
|
123
|
+
when NODE_CDATA then Leptris::XML::CDATA.new(c_ptr, document)
|
|
124
|
+
when NODE_PI then Leptris::XML::ProcessingInstruction.new(c_ptr, document)
|
|
125
|
+
else new(c_ptr, document)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def name
|
|
130
|
+
raise NotImplementedError
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def content
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def type
|
|
138
|
+
FFI.leptris_node_get_type(@c_ptr)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def element?; type == NODE_ELEMENT; end
|
|
142
|
+
def text?; type == NODE_TEXT; end
|
|
143
|
+
def comment?; type == NODE_COMMENT; end
|
|
144
|
+
def cdata?; type == NODE_CDATA; end
|
|
145
|
+
def processing_instruction?; type == NODE_PI; end
|
|
146
|
+
|
|
147
|
+
def next_sibling
|
|
148
|
+
ptr = FFI.leptris_node_next_sibling(@c_ptr)
|
|
149
|
+
return nil if ptr.null?
|
|
150
|
+
Node.wrap(ptr, @document)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def previous_sibling
|
|
154
|
+
ptr = FFI.leptris_node_previous_sibling(@c_ptr)
|
|
155
|
+
return nil if ptr.null?
|
|
156
|
+
Node.wrap(ptr, @document)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def parent
|
|
160
|
+
ptr = FFI.leptris_element_parent(@c_ptr) # only works for elements
|
|
161
|
+
return nil if ptr.null?
|
|
162
|
+
Element.wrap(ptr, @document)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def children
|
|
166
|
+
NodeSet.new(@document, self)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def child
|
|
170
|
+
ptr = FFI.leptris_node_first_child(@c_ptr)
|
|
171
|
+
return nil if ptr.null?
|
|
172
|
+
Node.wrap(ptr, @document)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def traverse(&block)
|
|
176
|
+
return enum_for(:traverse) unless block_given?
|
|
177
|
+
block.call(self)
|
|
178
|
+
children.each { |c| c.traverse(&block) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
include Searchable
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Element
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
class Leptris::XML::Element < Leptris::XML::Node
|
|
189
|
+
def name
|
|
190
|
+
FFI.leptris_element_name(@c_ptr)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def name=(n)
|
|
194
|
+
FFI.leptris_element_set_name(@c_ptr, n)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def content
|
|
198
|
+
FFI.leptris_element_text(@c_ptr)
|
|
199
|
+
end
|
|
200
|
+
alias_method :text, :content
|
|
201
|
+
|
|
202
|
+
def [](attr_name)
|
|
203
|
+
FFI.leptris_element_attribute(@c_ptr, attr_name, nil)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def []=(attr_name, value)
|
|
207
|
+
FFI.leptris_element_set_attribute(@c_ptr, attr_name, value.to_s)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def attributes
|
|
211
|
+
# Walk the C attribute list, build a hash of Attr objects
|
|
212
|
+
result = {}
|
|
213
|
+
count = FFI.leptris_element_attribute_count(@c_ptr)
|
|
214
|
+
# ... iterate attribute linked list ...
|
|
215
|
+
result
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def keys
|
|
219
|
+
attributes.keys
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def values
|
|
223
|
+
attributes.values.map(&:value)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def first_element_child
|
|
227
|
+
ptr = FFI.leptris_element_first_child_any(@c_ptr)
|
|
228
|
+
return nil if ptr.null?
|
|
229
|
+
Element.wrap(ptr, @document)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def add_child(node)
|
|
233
|
+
FFI.leptris_element_append_child(@c_ptr, node.c_ptr)
|
|
234
|
+
node
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def add_class(names) ... end
|
|
238
|
+
def remove_class(names = nil) ... end
|
|
239
|
+
def classes
|
|
240
|
+
(self['class'] || '').split
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## NodeSet
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
class Leptris::XML::NodeSet
|
|
249
|
+
include Enumerable
|
|
250
|
+
include Searchable
|
|
251
|
+
|
|
252
|
+
def initialize(document, result_ptr = nil)
|
|
253
|
+
@document = document
|
|
254
|
+
@result_ptr = result_ptr # LeptrisXPathResult pointer
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def length
|
|
258
|
+
return @cached_length if @cached_length
|
|
259
|
+
@cached_length =
|
|
260
|
+
@result_ptr ? FFI.leptris_xpath_result_count(@result_ptr) : 0
|
|
261
|
+
end
|
|
262
|
+
alias_method :size, :length
|
|
263
|
+
|
|
264
|
+
def [](index)
|
|
265
|
+
return nil if index < 0 || index >= length
|
|
266
|
+
ptr = FFI.leptris_xpath_result_get(@result_ptr, index)
|
|
267
|
+
return nil if ptr.null?
|
|
268
|
+
Node.wrap(ptr, @document)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def first(n = nil)
|
|
272
|
+
return self[0] if n.nil?
|
|
273
|
+
NodeSet.new(@document).tap { |ns| n.times { |i| ns << self[i] } }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def last
|
|
277
|
+
self[length - 1]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def each
|
|
281
|
+
return enum_for(:each) unless block_given?
|
|
282
|
+
length.times { |i| yield self[i] }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def empty?
|
|
286
|
+
length == 0
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def inner_text
|
|
290
|
+
map(&:content).join
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def to_xml
|
|
294
|
+
map(&:to_xml).join
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def free
|
|
298
|
+
return unless @result_ptr
|
|
299
|
+
FFI.leptris_xpath_result_free(@result_ptr)
|
|
300
|
+
@result_ptr = nil
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Searchable (mixin for xpath/css)
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
module Leptris::XML::Searchable
|
|
309
|
+
def xpath(*paths)
|
|
310
|
+
expr = paths.join(' | ')
|
|
311
|
+
result_ptr = FFI.leptris_xpath_eval(
|
|
312
|
+
document.c_ptr,
|
|
313
|
+
respond_to?(:c_ptr) ? c_ptr : nil,
|
|
314
|
+
expr
|
|
315
|
+
)
|
|
316
|
+
return nil if result_ptr.null?
|
|
317
|
+
|
|
318
|
+
result_type = FFI.leptris_xpath_result_type(result_ptr)
|
|
319
|
+
case result_type
|
|
320
|
+
when XPATH_NODESET
|
|
321
|
+
NodeSet.new(@document, result_ptr)
|
|
322
|
+
when XPATH_NUMBER
|
|
323
|
+
n = FFI.leptris_xpath_result_number(result_ptr)
|
|
324
|
+
FFI.leptris_xpath_result_free(result_ptr)
|
|
325
|
+
n
|
|
326
|
+
when XPATH_STRING
|
|
327
|
+
s = FFI.leptris_xpath_result_string(result_ptr)
|
|
328
|
+
FFI.leptris_xpath_result_free(result_ptr)
|
|
329
|
+
s
|
|
330
|
+
when XPATH_BOOLEAN
|
|
331
|
+
b = FFI.leptris_xpath_result_boolean(result_ptr)
|
|
332
|
+
FFI.leptris_xpath_result_free(result_ptr)
|
|
333
|
+
b == 1
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def at_xpath(*paths)
|
|
338
|
+
ns = xpath(*paths)
|
|
339
|
+
ns.is_a?(NodeSet) ? ns.first : ns
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def search(*args)
|
|
343
|
+
expr = args.first.to_s
|
|
344
|
+
if expr.start_with?('/') || expr.start_with?('//')
|
|
345
|
+
xpath(expr)
|
|
346
|
+
else
|
|
347
|
+
css(expr)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def at(*args)
|
|
352
|
+
result = search(*args)
|
|
353
|
+
result.is_a?(NodeSet) ? result.first : result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def css(*selectors)
|
|
357
|
+
# Convert CSS to XPath (minimal converter)
|
|
358
|
+
xpath_expr = CssToXPath.convert(selectors.join(', '))
|
|
359
|
+
xpath(xpath_expr)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def at_css(*selectors)
|
|
363
|
+
result = css(*selectors)
|
|
364
|
+
result.is_a?(NodeSet) ? result.first : result
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Implementation notes
|
|
370
|
+
|
|
371
|
+
- **No `require_relative`** anywhere. Use `autoload` defined in the
|
|
372
|
+
immediate parent namespace's file (e.g., `lib/leptris/xml.rb`).
|
|
373
|
+
- **No `instance_variable_set`/`instance_variable_get`** on other
|
|
374
|
+
objects. Use public accessor methods.
|
|
375
|
+
- **No `send`** to call private methods.
|
|
376
|
+
- **No `respond_to?`** for type checks. Use `is_a?`.
|
|
377
|
+
- Wrap C pointers in Ruby objects via `Node.wrap(ptr, doc)` which
|
|
378
|
+
dispatches on the C node type.
|
|
379
|
+
- Node objects are lightweight: just a pointer + document reference.
|
|
380
|
+
No caching of properties (each call goes through FFI).
|
|
381
|
+
- The document is the ONLY object that owns memory. All other objects
|
|
382
|
+
are non-owning handles.
|