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,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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lightweight attribute wrapper. libleptris v0.4.4 doesn't expose per-attribute
|
|
4
|
+
# pointers (no LeptrisAttribute accessors in the public API), so an Attr is a
|
|
5
|
+
# (name, value, parent_element) triple. Mutation goes through the parent
|
|
6
|
+
# element via #[]. Once upstream adds attribute-level accessors, this can
|
|
7
|
+
# become a pointer-backed wrapper.
|
|
8
|
+
|
|
9
|
+
class Leptris::XML::Attr
|
|
10
|
+
attr_reader :name, :value, :element
|
|
11
|
+
|
|
12
|
+
def initialize(name, value, element)
|
|
13
|
+
@name = name
|
|
14
|
+
@value = value
|
|
15
|
+
@element = element
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def value=(new_value)
|
|
19
|
+
@element[@name] = new_value
|
|
20
|
+
@value = new_value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def namespace
|
|
24
|
+
nil # upstream gap: per-attribute namespace not exposed
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def remove
|
|
28
|
+
@element.remove_attribute(@name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_s; @value; end
|
|
32
|
+
def to_str; @value; end
|
|
33
|
+
|
|
34
|
+
def ==(other)
|
|
35
|
+
other.is_a?(Leptris::XML::Attr) &&
|
|
36
|
+
@name == other.name && @value == other.value &&
|
|
37
|
+
@element == other.element
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<#{self.class.name} name=#{@name.inspect} value=#{@value.inspect}>"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Leptris
|
|
4
|
+
module XML
|
|
5
|
+
# @api private
|
|
6
|
+
#
|
|
7
|
+
# Builds a NULL-terminated `const char**` from a Ruby array of strings,
|
|
8
|
+
# suitable for passing to leptris_c14n_canonicalize_ex's
|
|
9
|
+
# `inclusive_ns_prefixes` argument. Returns [pointer, anchor] where
|
|
10
|
+
# anchor is the underlying MemoryPointer that the caller must keep
|
|
11
|
+
# alive (e.g. via local variable) for the duration of the FFI call.
|
|
12
|
+
def self.c14n_build_ns_pointer(inclusive_namespaces)
|
|
13
|
+
return [nil, nil] if inclusive_namespaces.nil? || inclusive_namespaces.empty?
|
|
14
|
+
strs = Array(inclusive_namespaces).map(&:to_s)
|
|
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
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
status = Leptris::XML::FFI.leptris_cdata_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
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
status = Leptris::XML::FFI.leptris_comment_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
|
|
@@ -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
|