leptris 1.1.2-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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +619 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +405 -0
- data/Rakefile +98 -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 +41 -0
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +15 -0
- data/lib/leptris/xml/comment.rb +15 -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 +212 -0
- data/lib/leptris/xml/document_fragment.rb +42 -0
- data/lib/leptris/xml/element.rb +259 -0
- data/lib/leptris/xml/ffi.rb +475 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/node.rb +211 -0
- data/lib/leptris/xml/node_set.rb +150 -0
- data/lib/leptris/xml/parse_options.rb +29 -0
- data/lib/leptris/xml/processing_instruction.rb +24 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/parser.rb +136 -0
- data/lib/leptris/xml/sax.rb +12 -0
- data/lib/leptris/xml/searchable.rb +92 -0
- data/lib/leptris/xml/serialization.rb +41 -0
- data/lib/leptris/xml/text.rb +15 -0
- data/lib/leptris/xml.rb +40 -0
- data/lib/leptris.rb +7 -0
- data/lib/libleptris.so +0 -0
- metadata +162 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
= XPath Performance Benchmarks
|
|
2
|
+
:toc:
|
|
3
|
+
:toclevels: 3
|
|
4
|
+
|
|
5
|
+
== Overview
|
|
6
|
+
|
|
7
|
+
This document provides comprehensive performance benchmarks, generated from actual benchmark runs using `benchmark-ips`.
|
|
8
|
+
|
|
9
|
+
**IMPORTANT**: This file is auto-generated from benchmark data. Do not edit manually.
|
|
10
|
+
To update, run the appropriate rake task (see footer).
|
|
11
|
+
|
|
12
|
+
== Test Environment
|
|
13
|
+
|
|
14
|
+
* **Ruby Version**: 3.1.1
|
|
15
|
+
* **Platform**: arm64-darwin21
|
|
16
|
+
* **Leptris Version**: 0.1.0
|
|
17
|
+
* **Libraries Available**:
|
|
18
|
+
* **Nokogiri**: Available
|
|
19
|
+
* **Timestamp**: 2025-11-27T18:27:43+0800
|
|
20
|
+
* **Method**: benchmark-ips
|
|
21
|
+
|
|
22
|
+
== Performance Summary
|
|
23
|
+
|
|
24
|
+
**Overall Results** (50 tests, vs Nokogiri):
|
|
25
|
+
|
|
26
|
+
* **Average**: 2.34× slower
|
|
27
|
+
* **Best case**: 3.13× faster
|
|
28
|
+
* **Worst case**: 10.38× slower
|
|
29
|
+
* **Tests faster**: 2/50 (4.0%)
|
|
30
|
+
* **Tests slower**: 48/50 (96.0%)
|
|
31
|
+
|
|
32
|
+
**Verdict**: ❌ **Needs Optimization** - Significantly slower than Nokogiri
|
|
33
|
+
|
|
34
|
+
== axes
|
|
35
|
+
|
|
36
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
37
|
+
|===
|
|
38
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
39
|
+
|
|
40
|
+
|Child
|
|
41
|
+
|1.39µs
|
|
42
|
+
|4.3µs
|
|
43
|
+
|**3.09× faster** ✅
|
|
44
|
+
|
|
45
|
+
|Descendant
|
|
46
|
+
|59.56µs
|
|
47
|
+
|5.74µs
|
|
48
|
+
|**10.38× slower** ❌
|
|
49
|
+
|
|
50
|
+
|Descendant Or Self
|
|
51
|
+
|17.69µs
|
|
52
|
+
|6.49µs
|
|
53
|
+
|**2.73× slower** ❌
|
|
54
|
+
|
|
55
|
+
|Parent
|
|
56
|
+
|11.5µs
|
|
57
|
+
|4.27µs
|
|
58
|
+
|**2.69× slower** ❌
|
|
59
|
+
|
|
60
|
+
|Ancestor
|
|
61
|
+
|12.5µs
|
|
62
|
+
|4.3µs
|
|
63
|
+
|**2.91× slower** ❌
|
|
64
|
+
|
|
65
|
+
|Ancestor Or Self
|
|
66
|
+
|12.72µs
|
|
67
|
+
|4.36µs
|
|
68
|
+
|**2.92× slower** ❌
|
|
69
|
+
|
|
70
|
+
|Self
|
|
71
|
+
|11.5µs
|
|
72
|
+
|7.53µs
|
|
73
|
+
|**1.53× slower** ❌
|
|
74
|
+
|
|
75
|
+
|Following Sibling
|
|
76
|
+
|11.83µs
|
|
77
|
+
|6.89µs
|
|
78
|
+
|**1.72× slower** ❌
|
|
79
|
+
|
|
80
|
+
|Preceding Sibling
|
|
81
|
+
|11.37µs
|
|
82
|
+
|5.52µs
|
|
83
|
+
|**2.06× slower** ❌
|
|
84
|
+
|
|
85
|
+
|Following
|
|
86
|
+
|17.99µs
|
|
87
|
+
|12.94µs
|
|
88
|
+
|**1.39× slower** ❌
|
|
89
|
+
|
|
90
|
+
|Preceding
|
|
91
|
+
|12.91µs
|
|
92
|
+
|5.3µs
|
|
93
|
+
|**2.44× slower** ❌
|
|
94
|
+
|
|
95
|
+
|Attribute
|
|
96
|
+
|11.29µs
|
|
97
|
+
|5.08µs
|
|
98
|
+
|**2.22× slower** ❌
|
|
99
|
+
|
|
100
|
+
|Namespace
|
|
101
|
+
|14.79µs
|
|
102
|
+
|7.71µs
|
|
103
|
+
|**1.92× slower** ❌
|
|
104
|
+
|
|
105
|
+
|===
|
|
106
|
+
|
|
107
|
+
== simple_paths
|
|
108
|
+
|
|
109
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
110
|
+
|===
|
|
111
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
112
|
+
|
|
113
|
+
|Absolute Path
|
|
114
|
+
|6.01µs
|
|
115
|
+
|4.99µs
|
|
116
|
+
|**1.2× slower** ❌
|
|
117
|
+
|
|
118
|
+
|Relative Path
|
|
119
|
+
|1.65µs
|
|
120
|
+
|4.66µs
|
|
121
|
+
|**2.82× faster** ✅
|
|
122
|
+
|
|
123
|
+
|Descendant Path
|
|
124
|
+
|12.73µs
|
|
125
|
+
|4.48µs
|
|
126
|
+
|**2.84× slower** ❌
|
|
127
|
+
|
|
128
|
+
|Wildcard
|
|
129
|
+
|10.55µs
|
|
130
|
+
|4.51µs
|
|
131
|
+
|**2.34× slower** ❌
|
|
132
|
+
|
|
133
|
+
|Attribute Access
|
|
134
|
+
|25.37µs
|
|
135
|
+
|4.76µs
|
|
136
|
+
|**5.33× slower** ❌
|
|
137
|
+
|
|
138
|
+
|Text Nodes
|
|
139
|
+
|12.85µs
|
|
140
|
+
|4.67µs
|
|
141
|
+
|**2.75× slower** ❌
|
|
142
|
+
|
|
143
|
+
|===
|
|
144
|
+
|
|
145
|
+
== position_predicates
|
|
146
|
+
|
|
147
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
148
|
+
|===
|
|
149
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
150
|
+
|
|
151
|
+
|First
|
|
152
|
+
|38.24µs
|
|
153
|
+
|5.95µs
|
|
154
|
+
|**6.43× slower** ❌
|
|
155
|
+
|
|
156
|
+
|Last
|
|
157
|
+
|14.06µs
|
|
158
|
+
|5.86µs
|
|
159
|
+
|**2.4× slower** ❌
|
|
160
|
+
|
|
161
|
+
|Position Eq
|
|
162
|
+
|11.34µs
|
|
163
|
+
|5.6µs
|
|
164
|
+
|**2.03× slower** ❌
|
|
165
|
+
|
|
166
|
+
|Position Range
|
|
167
|
+
|11.83µs
|
|
168
|
+
|5.56µs
|
|
169
|
+
|**2.13× slower** ❌
|
|
170
|
+
|
|
171
|
+
|===
|
|
172
|
+
|
|
173
|
+
== boolean_predicates
|
|
174
|
+
|
|
175
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
176
|
+
|===
|
|
177
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
178
|
+
|
|
179
|
+
|Attribute Exists
|
|
180
|
+
|11.91µs
|
|
181
|
+
|5.77µs
|
|
182
|
+
|**2.06× slower** ❌
|
|
183
|
+
|
|
184
|
+
|Attribute Value
|
|
185
|
+
|12.15µs
|
|
186
|
+
|7.24µs
|
|
187
|
+
|**1.68× slower** ❌
|
|
188
|
+
|
|
189
|
+
|Element Exists
|
|
190
|
+
|13.13µs
|
|
191
|
+
|7.57µs
|
|
192
|
+
|**1.73× slower** ❌
|
|
193
|
+
|
|
194
|
+
|Negation
|
|
195
|
+
|11.87µs
|
|
196
|
+
|6.21µs
|
|
197
|
+
|**1.91× slower** ❌
|
|
198
|
+
|
|
199
|
+
|===
|
|
200
|
+
|
|
201
|
+
== multiple_predicates
|
|
202
|
+
|
|
203
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
204
|
+
|===
|
|
205
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
206
|
+
|
|
207
|
+
|Two Predicates
|
|
208
|
+
|16.65µs
|
|
209
|
+
|6.86µs
|
|
210
|
+
|**2.43× slower** ❌
|
|
211
|
+
|
|
212
|
+
|Position And Boolean
|
|
213
|
+
|11.52µs
|
|
214
|
+
|11.17µs
|
|
215
|
+
|**1.03× slower** ❌
|
|
216
|
+
|
|
217
|
+
|Three Predicates
|
|
218
|
+
|16.73µs
|
|
219
|
+
|11.11µs
|
|
220
|
+
|**1.51× slower** ❌
|
|
221
|
+
|
|
222
|
+
|===
|
|
223
|
+
|
|
224
|
+
== nodeset_functions
|
|
225
|
+
|
|
226
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
227
|
+
|===
|
|
228
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
229
|
+
|
|
230
|
+
|Count
|
|
231
|
+
|10.64µs
|
|
232
|
+
|3.69µs
|
|
233
|
+
|**2.88× slower** ❌
|
|
234
|
+
|
|
235
|
+
|Last Func
|
|
236
|
+
|17.68µs
|
|
237
|
+
|5.06µs
|
|
238
|
+
|**3.49× slower** ❌
|
|
239
|
+
|
|
240
|
+
|Position Func
|
|
241
|
+
|12.61µs
|
|
242
|
+
|8.05µs
|
|
243
|
+
|**1.57× slower** ❌
|
|
244
|
+
|
|
245
|
+
|Local Name
|
|
246
|
+
|19.11µs
|
|
247
|
+
|13.09µs
|
|
248
|
+
|**1.46× slower** ❌
|
|
249
|
+
|
|
250
|
+
|Name
|
|
251
|
+
|15.31µs
|
|
252
|
+
|13.7µs
|
|
253
|
+
|**1.12× slower** ❌
|
|
254
|
+
|
|
255
|
+
|===
|
|
256
|
+
|
|
257
|
+
== string_functions
|
|
258
|
+
|
|
259
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
260
|
+
|===
|
|
261
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
262
|
+
|
|
263
|
+
|String
|
|
264
|
+
|13.64µs
|
|
265
|
+
|11.58µs
|
|
266
|
+
|**1.18× slower** ❌
|
|
267
|
+
|
|
268
|
+
|Concat
|
|
269
|
+
|43.93µs
|
|
270
|
+
|12.81µs
|
|
271
|
+
|**3.43× slower** ❌
|
|
272
|
+
|
|
273
|
+
|Starts With
|
|
274
|
+
|20.35µs
|
|
275
|
+
|7.64µs
|
|
276
|
+
|**2.66× slower** ❌
|
|
277
|
+
|
|
278
|
+
|Contains
|
|
279
|
+
|14.53µs
|
|
280
|
+
|9.3µs
|
|
281
|
+
|**1.56× slower** ❌
|
|
282
|
+
|
|
283
|
+
|String Length
|
|
284
|
+
|15.21µs
|
|
285
|
+
|8.94µs
|
|
286
|
+
|**1.7× slower** ❌
|
|
287
|
+
|
|
288
|
+
|===
|
|
289
|
+
|
|
290
|
+
== boolean_functions
|
|
291
|
+
|
|
292
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
293
|
+
|===
|
|
294
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
295
|
+
|
|
296
|
+
|Boolean
|
|
297
|
+
|10.76µs
|
|
298
|
+
|4.56µs
|
|
299
|
+
|**2.36× slower** ❌
|
|
300
|
+
|
|
301
|
+
|Not
|
|
302
|
+
|12.34µs
|
|
303
|
+
|6.8µs
|
|
304
|
+
|**1.81× slower** ❌
|
|
305
|
+
|
|
306
|
+
|True
|
|
307
|
+
|11.11µs
|
|
308
|
+
|5.15µs
|
|
309
|
+
|**2.16× slower** ❌
|
|
310
|
+
|
|
311
|
+
|===
|
|
312
|
+
|
|
313
|
+
== number_functions
|
|
314
|
+
|
|
315
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
316
|
+
|===
|
|
317
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
318
|
+
|
|
319
|
+
|Number
|
|
320
|
+
|12.6µs
|
|
321
|
+
|7.06µs
|
|
322
|
+
|**1.78× slower** ❌
|
|
323
|
+
|
|
324
|
+
|Sum
|
|
325
|
+
|14.71µs
|
|
326
|
+
|4.51µs
|
|
327
|
+
|**3.26× slower** ❌
|
|
328
|
+
|
|
329
|
+
|Floor
|
|
330
|
+
|18.48µs
|
|
331
|
+
|11.28µs
|
|
332
|
+
|**1.64× slower** ❌
|
|
333
|
+
|
|
334
|
+
|Ceiling
|
|
335
|
+
|22.09µs
|
|
336
|
+
|10.11µs
|
|
337
|
+
|**2.18× slower** ❌
|
|
338
|
+
|
|
339
|
+
|===
|
|
340
|
+
|
|
341
|
+
== complex_queries
|
|
342
|
+
|
|
343
|
+
[cols="["3", "2", "2", "2"]",options="header"]
|
|
344
|
+
|===
|
|
345
|
+
|Test |Leptris |Nokogiri |vs Nokogiri
|
|
346
|
+
|
|
347
|
+
|Multi Axis
|
|
348
|
+
|12.58µs
|
|
349
|
+
|6.44µs
|
|
350
|
+
|**1.95× slower** ❌
|
|
351
|
+
|
|
352
|
+
|Union
|
|
353
|
+
|24.42µs
|
|
354
|
+
|13.61µs
|
|
355
|
+
|**1.79× slower** ❌
|
|
356
|
+
|
|
357
|
+
|Nested
|
|
358
|
+
|18.96µs
|
|
359
|
+
|12.73µs
|
|
360
|
+
|**1.49× slower** ❌
|
|
361
|
+
|
|
362
|
+
|===
|
|
363
|
+
|
|
364
|
+
== Conclusion
|
|
365
|
+
|
|
366
|
+
Based on 50 comprehensive tests (vs Nokogiri):
|
|
367
|
+
|
|
368
|
+
* **Average Performance**: 2.34× slower
|
|
369
|
+
* **Tests faster**: 2/50 (4.0%)
|
|
370
|
+
* **Tests slower**: 48/50 (96.0%)
|
|
371
|
+
* **Best result**: 0.32×
|
|
372
|
+
* **Worst result**: 10.38×
|
|
373
|
+
|
|
374
|
+
❌ **Leptris performance needs optimization** - Significantly slower than Nokogiri (2.34× average).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
**Generated**: 2025-11-27T18:27:43+0800
|
|
379
|
+
**Baseline**: Nokogiri
|
data/leptris.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/leptris/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "leptris"
|
|
7
|
+
spec.version = Leptris::VERSION
|
|
8
|
+
spec.authors = ["Ribose Inc."]
|
|
9
|
+
spec.email = ["open.source@ribose.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Nokogiri-compatible Ruby binding for libleptris (XML 1.0, XPath 1.0, SAX)"
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
Leptris is a Nokogiri-compatible Ruby binding for libleptris, a pure-C99 XML
|
|
14
|
+
1.0 parser with full XPath 1.0 and SAX support. The C DOM is the single
|
|
15
|
+
source of truth; Ruby objects are thin FFI wrappers (one Ruby method =
|
|
16
|
+
one FFI call).
|
|
17
|
+
DESC
|
|
18
|
+
spec.homepage = "https://github.com/leptris/leptris-ruby"
|
|
19
|
+
spec.license = "MIT"
|
|
20
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
21
|
+
|
|
22
|
+
spec.metadata = {
|
|
23
|
+
"homepage_uri" => spec.homepage,
|
|
24
|
+
"source_code_uri" => "https://github.com/leptris/leptris-ruby",
|
|
25
|
+
"changelog_uri" => "https://github.com/leptris/leptris-ruby/blob/main/CHANGELOG.md",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
spec.files = Dir.chdir(__dir__) do
|
|
29
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
|
+
(File.expand_path(f) == __FILE__) ||
|
|
31
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
spec.bindir = "bin"
|
|
35
|
+
spec.executables = []
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
spec.add_dependency "ffi", "~> 1.15"
|
|
39
|
+
|
|
40
|
+
spec.add_development_dependency "rake"
|
|
41
|
+
spec.add_development_dependency "rspec"
|
|
42
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lightweight attribute wrapper: a (name, value, parent_element) triple
|
|
4
|
+
# materialized from the attribute-iteration face. Mutation goes through
|
|
5
|
+
# the parent element via #[].
|
|
6
|
+
|
|
7
|
+
class Leptris::XML::Attr
|
|
8
|
+
attr_reader :name, :value, :element
|
|
9
|
+
|
|
10
|
+
def initialize(name, value, element)
|
|
11
|
+
@name = name
|
|
12
|
+
@value = value
|
|
13
|
+
@element = element
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def value=(new_value)
|
|
17
|
+
@element[@name] = new_value
|
|
18
|
+
@value = new_value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def namespace
|
|
22
|
+
nil # upstream gap: per-attribute namespace not exposed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def remove
|
|
26
|
+
@element.remove_attribute(@name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s; @value; end
|
|
30
|
+
def to_str; @value; end
|
|
31
|
+
|
|
32
|
+
def ==(other)
|
|
33
|
+
other.is_a?(Leptris::XML::Attr) &&
|
|
34
|
+
@name == other.name && @value == other.value &&
|
|
35
|
+
@element == other.element
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inspect
|
|
39
|
+
"#<#{self.class.name} name=#{@name.inspect} value=#{@value.inspect}>"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapter for libleptris's NULL-terminated char** wire format. Both
|
|
4
|
+
# directions of the conversion live here so the pointer arithmetic has
|
|
5
|
+
# one home (previously c14n built the array by hand and SAX walked it
|
|
6
|
+
# by hand).
|
|
7
|
+
module Leptris::XML::CStringArray
|
|
8
|
+
# Ruby strings -> NULL-terminated char**. Returns [buffer, anchors];
|
|
9
|
+
# the caller must keep both referenced for the duration of the FFI
|
|
10
|
+
# call (e.g. via local variables).
|
|
11
|
+
def self.to_c(strings)
|
|
12
|
+
strs = Array(strings).map(&:to_s)
|
|
13
|
+
return [nil, nil] if strs.empty?
|
|
14
|
+
|
|
15
|
+
ptr_size = ::FFI::type_size(:pointer)
|
|
16
|
+
buffer = ::FFI::MemoryPointer.new(:pointer, strs.size + 1)
|
|
17
|
+
anchors = strs.map { |s| ::FFI::MemoryPointer.from_string(s) }
|
|
18
|
+
anchors.each_with_index { |p, i| buffer.put_pointer(i * ptr_size, p) }
|
|
19
|
+
buffer.put_pointer(strs.size * ptr_size, nil)
|
|
20
|
+
[buffer, anchors]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# NULL-terminated char** -> Ruby strings. Stops at the first NULL
|
|
24
|
+
# entry. Returns [] for a NULL pointer.
|
|
25
|
+
def self.to_ruby(ptr)
|
|
26
|
+
return [] if ptr.nil? || ptr.null?
|
|
27
|
+
|
|
28
|
+
ptr_size = ::FFI::type_size(:pointer)
|
|
29
|
+
result = []
|
|
30
|
+
offset = 0
|
|
31
|
+
while (entry = ptr.get_pointer(offset)) && !entry.null?
|
|
32
|
+
result << entry.read_string
|
|
33
|
+
offset += ptr_size
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::CDATA < Leptris::XML::Text
|
|
4
|
+
def name; "#cdata-section"; end
|
|
5
|
+
|
|
6
|
+
def content
|
|
7
|
+
Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content=(new_content)
|
|
11
|
+
Leptris::XML::FFI.check_status(
|
|
12
|
+
Leptris::XML::FFI.leptris_cdata_node_set_content(@c_ptr, new_content.to_s))
|
|
13
|
+
new_content
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::Comment < Leptris::XML::Node
|
|
4
|
+
def name; "comment"; end
|
|
5
|
+
|
|
6
|
+
def content
|
|
7
|
+
Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content=(new_content)
|
|
11
|
+
Leptris::XML::FFI.check_status(
|
|
12
|
+
Leptris::XML::FFI.leptris_comment_node_set_content(@c_ptr, new_content.to_s))
|
|
13
|
+
new_content
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Leptris
|
|
4
|
+
module XML
|
|
5
|
+
# Minimal CSS-to-XPath translator covering the common Nokogiri subset:
|
|
6
|
+
#
|
|
7
|
+
# tag → //tag
|
|
8
|
+
# * → //*
|
|
9
|
+
# .class → //*[contains(concat(' ',@class,' '),' class ')]
|
|
10
|
+
# #id → //*[@id='id']
|
|
11
|
+
# [attr] → //*[@attr]
|
|
12
|
+
# [attr=val] → //*[@attr='val']
|
|
13
|
+
# tag[attr] → //tag[@attr]
|
|
14
|
+
# tag[attr=val] → //tag[@attr='val']
|
|
15
|
+
# parent > child → //parent/child
|
|
16
|
+
# ancestor descendant → //ancestor//descendant
|
|
17
|
+
# a, b → //a | //b
|
|
18
|
+
# tag:first-child → //tag[position()=1]
|
|
19
|
+
# tag:last-child → //tag[position()=last()]
|
|
20
|
+
# tag:not(simple) → //tag[not(self::simple)]
|
|
21
|
+
#
|
|
22
|
+
# For anything beyond this subset, raise ArgumentError. Callers can fall
|
|
23
|
+
# back to writing XPath directly via #xpath/#at_xpath.
|
|
24
|
+
module CssToXPath
|
|
25
|
+
COMMA_SPLIT = /\s*,\s*/.freeze
|
|
26
|
+
private_constant :COMMA_SPLIT
|
|
27
|
+
|
|
28
|
+
# Token types in a single simple selector.
|
|
29
|
+
# tag (optional), then zero or more of: .class, #id, [attr...], :pseudo
|
|
30
|
+
TAG_RE = /\A(\*|[\w-]+)/.freeze
|
|
31
|
+
DOT_CLASS_RE = /\A\.([\w-]+)/.freeze
|
|
32
|
+
HASH_ID_RE = /\A#([\w-]+)/.freeze
|
|
33
|
+
BRACKET_RE = /\A\[([^\]]+)\]/.freeze
|
|
34
|
+
PSEUDO_RE = /\A:([\w-]+(?:\([^)]*\))?)/.freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def convert(rule)
|
|
39
|
+
rule.to_s.split(COMMA_SPLIT).map { |r| convert_one(r.strip) }.join(" | ")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def convert_one(rule)
|
|
43
|
+
return "//*" if rule == "*"
|
|
44
|
+
|
|
45
|
+
# Tokenize chain first (handles > and whitespace)
|
|
46
|
+
if rule =~ /\s/ || rule.include?(">")
|
|
47
|
+
return convert_chain(rule)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
convert_simple(rule, prefix: "//")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Parse a single simple selector into (tag, predicates) where
|
|
54
|
+
# predicates is an array of XPath fragments to be joined via [p1][p2]...
|
|
55
|
+
def parse_simple(part)
|
|
56
|
+
tag = "*"
|
|
57
|
+
preds = []
|
|
58
|
+
|
|
59
|
+
s = part.strip
|
|
60
|
+
if s =~ TAG_RE
|
|
61
|
+
tag = $1
|
|
62
|
+
s = $'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
until s.empty?
|
|
66
|
+
case s
|
|
67
|
+
when DOT_CLASS_RE
|
|
68
|
+
preds << "contains(concat(' ',normalize-space(@class),' '),' #{$1} ')"
|
|
69
|
+
s = $'
|
|
70
|
+
when HASH_ID_RE
|
|
71
|
+
preds << "@id='#{$1}'"
|
|
72
|
+
s = $'
|
|
73
|
+
when BRACKET_RE
|
|
74
|
+
preds << convert_attrs($1)
|
|
75
|
+
s = $'
|
|
76
|
+
when PSEUDO_RE
|
|
77
|
+
preds << convert_pseudo($1)
|
|
78
|
+
s = $'
|
|
79
|
+
else
|
|
80
|
+
raise ArgumentError,
|
|
81
|
+
"unsupported CSS selector at #{s.inspect} (in #{part.inspect})"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
[tag, preds]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def convert_simple(part, prefix:)
|
|
89
|
+
tag, preds = parse_simple(part)
|
|
90
|
+
"#{prefix}#{tag}#{preds.map { |p| "[#{p}]" }.join}"
|
|
91
|
+
end
|
|
92
|
+
private_class_method :convert_simple
|
|
93
|
+
|
|
94
|
+
def convert_attrs(clause)
|
|
95
|
+
case clause.strip
|
|
96
|
+
when /\A(\w+)\z/
|
|
97
|
+
"@#{$1}"
|
|
98
|
+
when /\A(\w+)=['"]?([^'"\]]+)['"]?\z/
|
|
99
|
+
"@#{$1}='#{$2}'"
|
|
100
|
+
when /\A(\w+)~=['"]?([^'"\]]+)['"]?\z/
|
|
101
|
+
"contains(concat(' ',normalize-space(@#{$1}),' '),' #{$2} ')"
|
|
102
|
+
when /\A(\w+)\^=['"]?([^'"\]]+)['"]?\z/
|
|
103
|
+
"starts-with(@#{$1}, '#{$2}')"
|
|
104
|
+
when /\A(\w+)\$=['"]?([^'"\]]+)['"]?\z/
|
|
105
|
+
"substring(@#{$1}, string-length(@#{$1}) - string-length('#{$2}') + 1) = '#{$2}'"
|
|
106
|
+
when /\A(\w+)\*=['"]?([^'"\]]+)['"]?\z/
|
|
107
|
+
"contains(@#{$1}, '#{$2}')"
|
|
108
|
+
else
|
|
109
|
+
raise ArgumentError, "unsupported attribute selector: [#{clause}]"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
private_class_method :convert_attrs
|
|
113
|
+
|
|
114
|
+
def convert_pseudo(pseudo)
|
|
115
|
+
case pseudo
|
|
116
|
+
when "first-child" then "not(preceding-sibling::*)"
|
|
117
|
+
when "last-child" then "not(following-sibling::*)"
|
|
118
|
+
when "only-child" then "not(preceding-sibling::* or following-sibling::*)"
|
|
119
|
+
when "empty" then "not(node())"
|
|
120
|
+
when "root" then "not(parent::*)"
|
|
121
|
+
when /\Anot\((.+)\)\z/
|
|
122
|
+
inner_tag, _ = parse_simple($1)
|
|
123
|
+
"not(self::#{inner_tag})"
|
|
124
|
+
else
|
|
125
|
+
raise ArgumentError, "unsupported pseudo-class: :#{pseudo}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
private_class_method :convert_pseudo
|
|
129
|
+
|
|
130
|
+
# Tokenize chain into [sel, op, sel, op, sel, ...] where op is :child or
|
|
131
|
+
# :descendant. Then build XPath.
|
|
132
|
+
def convert_chain(rule)
|
|
133
|
+
tokens = tokenize_chain(rule)
|
|
134
|
+
build_chain_xpath(tokens)
|
|
135
|
+
end
|
|
136
|
+
private_class_method :convert_chain
|
|
137
|
+
|
|
138
|
+
def tokenize_chain(rule)
|
|
139
|
+
tokens = []
|
|
140
|
+
s = rule.strip
|
|
141
|
+
until s.empty?
|
|
142
|
+
if s.sub!(/\A\s*>\s*/, "")
|
|
143
|
+
tokens << :child
|
|
144
|
+
elsif s.sub!(/\A\s+/, "")
|
|
145
|
+
# whitespace descendant only counts if previous token is a string
|
|
146
|
+
tokens << :descendant if tokens.last.is_a?(String)
|
|
147
|
+
else
|
|
148
|
+
# Read one simple selector — keep going until whitespace or > or end
|
|
149
|
+
m = s.match(/\A[^>\s]+/)
|
|
150
|
+
raise ArgumentError, "couldn't parse CSS chain at: #{s.inspect}" unless m
|
|
151
|
+
tokens << m[0]
|
|
152
|
+
s = m.post_match
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
tokens
|
|
156
|
+
end
|
|
157
|
+
private_class_method :tokenize_chain
|
|
158
|
+
|
|
159
|
+
def build_chain_xpath(tokens)
|
|
160
|
+
first = tokens.shift
|
|
161
|
+
raise ArgumentError, "empty CSS chain" unless first.is_a?(String)
|
|
162
|
+
|
|
163
|
+
xpath = convert_simple(first, prefix: "//")
|
|
164
|
+
until tokens.empty?
|
|
165
|
+
op = tokens.shift
|
|
166
|
+
sel = tokens.shift
|
|
167
|
+
raise ArgumentError, "malformed CSS chain" unless sel.is_a?(String)
|
|
168
|
+
inner_tag, preds = parse_simple(sel)
|
|
169
|
+
connector = op == :child ? "/" : "//"
|
|
170
|
+
xpath = "#{xpath}#{connector}#{inner_tag}#{preds.map { |p| "[#{p}]" }.join}"
|
|
171
|
+
end
|
|
172
|
+
xpath
|
|
173
|
+
end
|
|
174
|
+
private_class_method :build_chain_xpath
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|