taurus 0.1.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 +518 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +1529 -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/taurus_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/taurus.bash +86 -0
- data/docs/completion/taurus.zsh +74 -0
- data/docs/man/taurus-format.1 +227 -0
- data/docs/man/taurus-parse.1 +178 -0
- data/docs/man/taurus-xpath.1 +312 -0
- data/docs/man/taurus.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/lib/taurus/version.rb +5 -0
- data/lib/taurus/xml/attr.rb +43 -0
- data/lib/taurus/xml/c14n.rb +23 -0
- data/lib/taurus/xml/cdata.rb +16 -0
- data/lib/taurus/xml/comment.rb +16 -0
- data/lib/taurus/xml/css_to_xpath.rb +177 -0
- data/lib/taurus/xml/doc_type.rb +54 -0
- data/lib/taurus/xml/document.rb +202 -0
- data/lib/taurus/xml/document_fragment.rb +42 -0
- data/lib/taurus/xml/element.rb +278 -0
- data/lib/taurus/xml/ffi.rb +420 -0
- data/lib/taurus/xml/namespace.rb +43 -0
- data/lib/taurus/xml/node.rb +221 -0
- data/lib/taurus/xml/node_set.rb +143 -0
- data/lib/taurus/xml/parse_options.rb +19 -0
- data/lib/taurus/xml/processing_instruction.rb +26 -0
- data/lib/taurus/xml/sax/document.rb +45 -0
- data/lib/taurus/xml/sax/parser.rb +148 -0
- data/lib/taurus/xml/sax.rb +12 -0
- data/lib/taurus/xml/searchable.rb +93 -0
- data/lib/taurus/xml/text.rb +16 -0
- data/lib/taurus/xml.rb +29 -0
- data/lib/taurus.rb +7 -0
- data/taurus.gemspec +42 -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
|
+
* **Taurus 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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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 |Taurus |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
|
+
❌ **Taurus performance needs optimization** - Significantly slower than Nokogiri (2.34× average).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
**Generated**: 2025-11-27T18:27:43+0800
|
|
379
|
+
**Baseline**: Nokogiri
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lightweight attribute wrapper. libtaurus v0.4.4 doesn't expose per-attribute
|
|
4
|
+
# pointers (no TaurusAttribute 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 Taurus::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?(Taurus::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 Taurus
|
|
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 taurus_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 Taurus::XML::CDATA < Taurus::XML::Text
|
|
4
|
+
def name; "#cdata-section"; end
|
|
5
|
+
|
|
6
|
+
def content
|
|
7
|
+
Taurus::XML::FFI.taurus_cdata_node_get_content(@c_ptr)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content=(new_content)
|
|
11
|
+
status = Taurus::XML::FFI.taurus_cdata_node_set_content(@c_ptr, new_content.to_s)
|
|
12
|
+
raise Taurus::XML::Error,
|
|
13
|
+
Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
|
|
14
|
+
new_content
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Taurus::XML::Comment < Taurus::XML::Node
|
|
4
|
+
def name; "comment"; end
|
|
5
|
+
|
|
6
|
+
def content
|
|
7
|
+
Taurus::XML::FFI.taurus_comment_node_get_content(@c_ptr)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content=(new_content)
|
|
11
|
+
status = Taurus::XML::FFI.taurus_comment_node_set_content(@c_ptr, new_content.to_s)
|
|
12
|
+
raise Taurus::XML::Error,
|
|
13
|
+
Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
|
|
14
|
+
new_content
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taurus
|
|
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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Taurus::XML::DocType
|
|
4
|
+
attr_reader :c_ptr, :document
|
|
5
|
+
|
|
6
|
+
def initialize(c_ptr, document)
|
|
7
|
+
@c_ptr = c_ptr
|
|
8
|
+
@document = document
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def name
|
|
12
|
+
Taurus::XML::FFI.taurus_doctype_get_name(@c_ptr)
|
|
13
|
+
end
|
|
14
|
+
alias_method :node_name, :name
|
|
15
|
+
|
|
16
|
+
def root_name
|
|
17
|
+
Taurus::XML::FFI.taurus_doctype_get_root_name(@c_ptr)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def public_id
|
|
21
|
+
Taurus::XML::FFI.taurus_doctype_get_public_id(@c_ptr)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def system_id
|
|
25
|
+
Taurus::XML::FFI.taurus_doctype_get_system_id(@c_ptr)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def internal_subset
|
|
29
|
+
Taurus::XML::FFI.taurus_doctype_get_internal_subset(@c_ptr)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def external_id
|
|
33
|
+
pub = public_id
|
|
34
|
+
sys = system_id
|
|
35
|
+
return nil if pub.nil? && sys.nil?
|
|
36
|
+
parts = []
|
|
37
|
+
parts << "PUBLIC \"#{pub}\"" if pub
|
|
38
|
+
parts << "SYSTEM \"#{sys}\"" if sys && pub.nil?
|
|
39
|
+
parts << "\"#{sys}\"" if pub && sys
|
|
40
|
+
parts.join(" ")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s
|
|
44
|
+
inner = internal_subset
|
|
45
|
+
parts = ["<!DOCTYPE #{name}"]
|
|
46
|
+
parts << external_id if external_id
|
|
47
|
+
parts << "[#{inner}]" if inner && !inner.empty?
|
|
48
|
+
parts.join(" ") + ">"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def inspect
|
|
52
|
+
"#<#{self.class.name} name=#{name.inspect} public=#{public_id.inspect} system=#{system_id.inspect}>"
|
|
53
|
+
end
|
|
54
|
+
end
|