leptris 1.6.0 → 1.6.2
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 +70 -0
- data/CLAUDE.md +76 -87
- data/README.adoc +80 -6
- data/Rakefile +1 -1
- data/benchmark/README.md +8 -164
- 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 +29 -14
- data/lib/leptris/xml/element.rb +21 -3
- data/lib/leptris/xml/ffi.rb +3 -13
- 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/searchable.rb +1 -1
- data/lib/leptris/xml/serialization.rb +20 -2
- data/lib/leptris/xml/text.rb +1 -0
- data/lib/leptris/xml.rb +3 -2
- metadata +1 -36
- 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
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
# TODO 5 — Serialization, C14N, memory management, specs, CSS
|
|
2
|
-
|
|
3
|
-
## Serialization
|
|
4
|
-
|
|
5
|
-
Wrap `leptris_serialize_document` and `leptris_c14n_canonicalize`.
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
# lib/leptris/xml/serialize_options.rb
|
|
9
|
-
class Leptris::XML::SerializeOptions < FFI::Struct
|
|
10
|
-
layout \
|
|
11
|
-
:indent, :int,
|
|
12
|
-
:xml_declaration, :int,
|
|
13
|
-
:no_empty_tags, :int,
|
|
14
|
-
:preserve_whitespace, :int
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Document#to_xml
|
|
18
|
-
def to_xml(options = {})
|
|
19
|
-
opts = SerializeOptions.new
|
|
20
|
-
opts[:indent] = options[:indent] || 0
|
|
21
|
-
opts[:xml_declaration] = options[:no_decl] ? 0 : 1
|
|
22
|
-
opts[:no_empty_tags] = options[:no_empty_tags] ? 1 : 0
|
|
23
|
-
opts[:preserve_whitespace] = options[:preserve_whitespace] ? 1 : 0
|
|
24
|
-
ptr = FFI.leptris_serialize_document(@c_ptr, opts.pointer)
|
|
25
|
-
return '' if ptr.nil? || ptr.null?
|
|
26
|
-
str = ptr.read_string
|
|
27
|
-
FFI.leptris_free_string(ptr)
|
|
28
|
-
str
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Node#to_xml (serialize just this subtree)
|
|
32
|
-
def to_xml(options = {})
|
|
33
|
-
# No C API for single-node serialization yet. Use Document serialize
|
|
34
|
-
# with a filter, or build the string manually. For v0.4.2, use the
|
|
35
|
-
# document-level serialize and post-process. This is a known limitation.
|
|
36
|
-
document.to_xml(options)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Node#inner_html
|
|
40
|
-
def inner_html
|
|
41
|
-
children.map { |c| c.to_xml }.join
|
|
42
|
-
end
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## C14N (Canonical XML)
|
|
46
|
-
|
|
47
|
-
```ruby
|
|
48
|
-
# lib/leptris/xml/c14n.rb
|
|
49
|
-
module Leptris::XML
|
|
50
|
-
C14N_1_0 = 0
|
|
51
|
-
C14N_1_1 = 1
|
|
52
|
-
C14N_EXCLUSIVE = 2
|
|
53
|
-
|
|
54
|
-
class Document
|
|
55
|
-
def canonicalize(mode = C14N_1_0, with_comments = false)
|
|
56
|
-
ptr = FFI.leptris_c14n_canonicalize(@c_ptr, mode, with_comments ? 1 : 0)
|
|
57
|
-
return '' if ptr.nil? || ptr.null?
|
|
58
|
-
str = ptr.read_string
|
|
59
|
-
FFI.leptris_free_string(ptr)
|
|
60
|
-
str
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Memory management
|
|
67
|
-
|
|
68
|
-
### Ownership rules
|
|
69
|
-
|
|
70
|
-
| Ruby class | Owns C memory? | Free function |
|
|
71
|
-
|-------------|----------------|---------------|
|
|
72
|
-
| Document | YES | `leptris_document_free` |
|
|
73
|
-
| Node/Element| NO (borrowed) | none (freed by Document) |
|
|
74
|
-
| NodeSet | YES (XPath result) | `leptris_xpath_result_free` |
|
|
75
|
-
| Attr | NO (borrowed) | none |
|
|
76
|
-
|
|
77
|
-
### Explicit free pattern
|
|
78
|
-
|
|
79
|
-
```ruby
|
|
80
|
-
doc = Leptris::XML.parse(xml)
|
|
81
|
-
begin
|
|
82
|
-
# ... work with doc ...
|
|
83
|
-
ensure
|
|
84
|
-
doc.free
|
|
85
|
-
end
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### GC safety net
|
|
89
|
-
|
|
90
|
-
```ruby
|
|
91
|
-
class Leptris::XML::Document
|
|
92
|
-
def self.wrap(ptr)
|
|
93
|
-
obj = allocate
|
|
94
|
-
obj.instance_variable_set(:@c_ptr, ptr)
|
|
95
|
-
ObjectSpace.define_finalizer(obj, finalizer(ptr))
|
|
96
|
-
obj
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def self.finalizer(ptr)
|
|
100
|
-
proc { FFI.leptris_document_free(ptr) if ptr && !ptr.null? }
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def free
|
|
104
|
-
return unless @c_ptr
|
|
105
|
-
FFI.leptris_document_free(@c_ptr)
|
|
106
|
-
@c_ptr = nil
|
|
107
|
-
# The finalizer still holds the old pointer but Document#free
|
|
108
|
-
# already freed it. Add a "freed" flag to detect double-free.
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
**IMPORTANT**: The finalizer must capture the POINTER VALUE, not the
|
|
114
|
-
Document object (which would prevent GC). Use `FFI::Pointer` directly
|
|
115
|
-
in the finalizer closure.
|
|
116
|
-
|
|
117
|
-
### Prevent use-after-free
|
|
118
|
-
|
|
119
|
-
```ruby
|
|
120
|
-
class Leptris::XML::Node
|
|
121
|
-
def c_ptr
|
|
122
|
-
raise UseAfterFreeError, "document has been freed" unless @document.c_ptr
|
|
123
|
-
@c_ptr
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Specs
|
|
129
|
-
|
|
130
|
-
### Structure
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
spec/
|
|
134
|
-
spec_helper.rb
|
|
135
|
-
xml/
|
|
136
|
-
parse_spec.rb
|
|
137
|
-
document_spec.rb
|
|
138
|
-
node_spec.rb
|
|
139
|
-
element_spec.rb
|
|
140
|
-
node_set_spec.rb
|
|
141
|
-
xpath_spec.rb
|
|
142
|
-
sax_spec.rb
|
|
143
|
-
serialize_spec.rb
|
|
144
|
-
c14n_spec.rb
|
|
145
|
-
memory_spec.rb
|
|
146
|
-
fixtures/
|
|
147
|
-
basic.xml
|
|
148
|
-
catalog.xml
|
|
149
|
-
namespaces.xml
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Test against Nokogiri behavior
|
|
153
|
-
|
|
154
|
-
Where Nokogiri's behavior is well-defined, match it exactly. The
|
|
155
|
-
specs should test:
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
# Parse
|
|
159
|
-
doc = Leptris::XML.parse('<root><child id="1">text</child></root>')
|
|
160
|
-
expect(doc.root.name).to eq('root')
|
|
161
|
-
expect(doc.root.children.first['id']).to eq('1')
|
|
162
|
-
|
|
163
|
-
# XPath
|
|
164
|
-
doc = Leptris::XML.parse('<lib><book/><book/></lib>')
|
|
165
|
-
expect(doc.xpath('count(//book)')).to eq(2.0)
|
|
166
|
-
expect(doc.xpath('//book').length).to eq(2)
|
|
167
|
-
expect(doc.at_xpath('//book')).to be_a(Leptris::XML::Element)
|
|
168
|
-
|
|
169
|
-
# Search
|
|
170
|
-
doc = Leptris::XML.parse('<root><a class="x"/><a class="y"/></root>')
|
|
171
|
-
expect(doc.search('a').length).to eq(2)
|
|
172
|
-
expect(doc.at('a')['class']).to eq('x')
|
|
173
|
-
|
|
174
|
-
# SAX
|
|
175
|
-
class Handler < Leptris::XML::SAX::Document
|
|
176
|
-
attr_reader :elements
|
|
177
|
-
def initialize; @elements = []; end
|
|
178
|
-
def start_element(name, attrs = []); @elements << name; end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
h = Handler.new
|
|
182
|
-
Leptris::XML::SAX::Parser.new(h).parse('<r><a/><b/></r>')
|
|
183
|
-
expect(h.elements).to eq(['r', 'a', 'b'])
|
|
184
|
-
|
|
185
|
-
# Serialize
|
|
186
|
-
doc = Leptris::XML.parse('<r/>')
|
|
187
|
-
expect(doc.to_xml).to match(/<r\/>/)
|
|
188
|
-
|
|
189
|
-
# C14N
|
|
190
|
-
expect(doc.canonicalize).to include('<r></r>')
|
|
191
|
-
|
|
192
|
-
# Memory
|
|
193
|
-
doc = Leptris::XML.parse('<r/>')
|
|
194
|
-
doc.free
|
|
195
|
-
expect { doc.root }.to raise_error(Leptris::XML::UseAfterFreeError)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Conformance
|
|
199
|
-
|
|
200
|
-
Run Nokogiri's own test suite against the Leptris binding where
|
|
201
|
-
possible. Skip tests for features Leptris doesn't support (HTML5,
|
|
202
|
-
XSLT, RelaxNG, DTD validation beyond what libleptris provides).
|
|
203
|
-
|
|
204
|
-
## CSS-to-XPath converter (minimal)
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
# lib/leptris/xml/css_to_xpath.rb
|
|
208
|
-
module Leptris::XML
|
|
209
|
-
module CssToXPath
|
|
210
|
-
def self.convert(rule)
|
|
211
|
-
parts = rule.strip.split(/\s+/)
|
|
212
|
-
xpath_parts = parts.map { |p| convert_part(p) }
|
|
213
|
-
'//' + xpath_parts.join('/')
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def self.convert_part(part)
|
|
217
|
-
# tag → tag
|
|
218
|
-
# .class → *[contains(concat(' ', @class,' '),' class ')]
|
|
219
|
-
# #id → *[@id='id']
|
|
220
|
-
# [attr] → *[@attr]
|
|
221
|
-
# [attr=val] → *[@attr='val']
|
|
222
|
-
# > child handled by split
|
|
223
|
-
# :first-child → *[position()=1]
|
|
224
|
-
# :last-child → *[position()=last()]
|
|
225
|
-
return '*' if part == '*'
|
|
226
|
-
|
|
227
|
-
if part.start_with?('.')
|
|
228
|
-
cls = part[1..]
|
|
229
|
-
"*[contains(concat(' ',normalize-space(@class),' '),' #{cls} ')]"
|
|
230
|
-
elsif part.start_with?('#')
|
|
231
|
-
id = part[1..]
|
|
232
|
-
"*[@id='#{id}']"
|
|
233
|
-
elsif match = part.match(/^(\w+)\[(\w+)='?([^'\]]+)'?\]$/i)
|
|
234
|
-
"#{match[1]}[@#{match[2]}='#{match[3]}']"
|
|
235
|
-
elsif match = part.match(/^(\w+)\[(\w+)\]$/i)
|
|
236
|
-
"#{match[1]}[@#{match[2]}]"
|
|
237
|
-
elsif match = part.match(/^(\w+):first-child$/i)
|
|
238
|
-
"#{match[1]}[position()=1]"
|
|
239
|
-
elsif match = part.match(/^(\w+):last-child$/i)
|
|
240
|
-
"#{match[1]}[position()=last()]"
|
|
241
|
-
else
|
|
242
|
-
part # pass through as tag name
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
This is a minimal converter. For full CSS3 support, integrate the
|
|
250
|
-
`css_parser` gem or port Nokogiri's CSS parser.
|
|
251
|
-
|
|
252
|
-
## File layout summary
|
|
253
|
-
|
|
254
|
-
```
|
|
255
|
-
lib/leptris.rb
|
|
256
|
-
lib/leptris/xml.rb
|
|
257
|
-
lib/leptris/xml/
|
|
258
|
-
ffi.rb
|
|
259
|
-
document.rb
|
|
260
|
-
node.rb
|
|
261
|
-
element.rb
|
|
262
|
-
text.rb
|
|
263
|
-
comment.rb
|
|
264
|
-
cdata.rb
|
|
265
|
-
processing_instruction.rb
|
|
266
|
-
attr.rb
|
|
267
|
-
node_set.rb
|
|
268
|
-
searchable.rb
|
|
269
|
-
parse_options.rb
|
|
270
|
-
serialize_options.rb
|
|
271
|
-
css_to_xpath.rb
|
|
272
|
-
sax.rb
|
|
273
|
-
sax/
|
|
274
|
-
parser.rb
|
|
275
|
-
document.rb
|
|
276
|
-
```
|