moxml 0.1.26 → 0.2.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/windows.yml +62 -0
  3. data/Gemfile +3 -1
  4. data/README.adoc +69 -6
  5. data/lib/compat/opal/moxml_boot.rb +5 -0
  6. data/lib/moxml/adapter/base.rb +53 -49
  7. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  8. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  9. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  10. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  11. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  12. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  13. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  14. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  15. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  16. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  17. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  18. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  19. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  20. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  21. data/lib/moxml/adapter/customized_oga.rb +2 -0
  22. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  23. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  24. data/lib/moxml/adapter/headed_ox.rb +9 -128
  25. data/lib/moxml/adapter/leptris.rb +965 -0
  26. data/lib/moxml/adapter/libxml.rb +131 -99
  27. data/lib/moxml/adapter/nokogiri.rb +29 -8
  28. data/lib/moxml/adapter/oga.rb +91 -54
  29. data/lib/moxml/adapter/ox.rb +93 -137
  30. data/lib/moxml/adapter/rexml.rb +61 -50
  31. data/lib/moxml/adapter.rb +2 -1
  32. data/lib/moxml/attribute.rb +8 -4
  33. data/lib/moxml/attribute_resolver.rb +189 -0
  34. data/lib/moxml/config.rb +29 -3
  35. data/lib/moxml/context.rb +12 -0
  36. data/lib/moxml/element.rb +29 -12
  37. data/lib/moxml/entity/reference.rb +28 -0
  38. data/lib/moxml/entity.rb +125 -0
  39. data/lib/moxml/node.rb +90 -1
  40. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  41. data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
  42. data/lib/moxml/version.rb +1 -1
  43. data/lib/moxml/xml_emitter.rb +71 -0
  44. data/lib/moxml/xpath/compiler.rb +60 -7
  45. data/lib/moxml/xpath/parser.rb +22 -15
  46. data/lib/moxml.rb +3 -0
  47. data/spec/examples/readme_examples_spec.rb +0 -4
  48. data/spec/examples/xpath_examples_spec.rb +0 -11
  49. data/spec/integration/all_adapters_spec.rb +17 -0
  50. data/spec/integration/sax_parity_spec.rb +58 -0
  51. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  52. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  53. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  54. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  55. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  56. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  57. data/spec/moxml/adapter/ox_spec.rb +20 -6
  58. data/spec/moxml/adapter/platform_spec.rb +15 -2
  59. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  60. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  61. data/spec/moxml/doctype_spec.rb +1 -1
  62. data/spec/moxml/entity_spec.rb +76 -0
  63. data/spec/moxml/node_spec.rb +104 -0
  64. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  65. data/spec/moxml/xml_emitter_spec.rb +64 -0
  66. data/spec/moxml/xpath/axes_spec.rb +72 -0
  67. data/spec/moxml/xpath/parser_spec.rb +6 -0
  68. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  69. data/spec/performance/benchmark_spec.rb +13 -6
  70. data/spec/performance/thread_safety_spec.rb +0 -4
  71. data/spec/spec_helper.rb +5 -1
  72. metadata +21 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d829252809a02c146a09b8458ec5d9a297e5a31e91e6b04b6b6eda27a31e30a9
4
- data.tar.gz: '09018d566262901ac5fe3fd864cd80e52dfe05f449e5b9e03054624044cab1af'
3
+ metadata.gz: 1e82117ebd56032bed4f9edffbb2714dec306b7bec52999193ef69fe62835ab9
4
+ data.tar.gz: 824fb9a07952249c2c97e650d5610d90c53ee23f279980aa5987e2a53922d74a
5
5
  SHA512:
6
- metadata.gz: 56cc005125bd3953a3e40f2867ef76f4bd2ea4823e3363904ff20330e67d62740ae26c444b10d6706d66d4c7b0f38ba4805e957cfbf6cb4cdf413a6480eefd03
7
- data.tar.gz: 58ec645d3a117ecd8ad275705fc03a345da7b52485b8119e8e0c0ebb12611c36dc01a12600b4871db050a23c7ac93d04b77632499307b94b5f7bb6000ccf366b
6
+ metadata.gz: 845a7de92860b25e68353e14efee1cc0015abfdc5a32f526b0baf1c45870e2966c1db6560eb45267a2fd0beab62936063493ac559fcca4317feffdcd0e54042a
7
+ data.tar.gz: 8242097334c0d1287bcc5c61658042e095ca1a34e375ff034c1504b9607f272937d62bd3ded2c439f45e7d6ae71ea58d86f5af37992858ab9a5fbadbe31fe738
@@ -0,0 +1,62 @@
1
+ name: windows
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ adapters:
14
+ name: Adapter specs on Windows (libxml)
15
+ runs-on: windows-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+ with:
20
+ submodules: "recursive"
21
+
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: "3.3"
26
+ bundler-cache: false
27
+
28
+ - name: Install libxml2 and build tools via MSYS2
29
+ shell: cmd
30
+ run: |
31
+ C:\msys64\usr\bin\pacman.exe -S --needed --noconfirm ^
32
+ mingw-w64-ucrt-x86_64-libxml2 ^
33
+ mingw-w64-ucrt-x86_64-libiconv ^
34
+ mingw-w64-ucrt-x86_64-zlib ^
35
+ mingw-w64-ucrt-x86_64-ragel ^
36
+ mingw-w64-ucrt-x86_64-make ^
37
+ mingw-w64-ucrt-x86_64-gcc
38
+
39
+ - name: Put MSYS2 UCRT64 toolchain on PATH
40
+ shell: pwsh
41
+ run: |
42
+ "PATH=C:\msys64\ucrt64\bin;C:\msys64\usr\bin;$env:PATH" >> $env:GITHUB_ENV
43
+
44
+ - name: Configure libxml-ruby build
45
+ shell: pwsh
46
+ run: |
47
+ bundle config build.libxml-ruby --with-xml2-include=C:/msys64/ucrt64/include/libxml2 --with-xml2-lib=C:/msys64/ucrt64/lib --with-iconv-include=C:/msys64/ucrt64/include --with-iconv-lib=C:/msys64/ucrt64/lib
48
+
49
+ - name: Generate ragel / ruby-ll outputs in vendored forks
50
+ shell: pwsh
51
+ run: |
52
+ gem install ruby-ll --no-document
53
+ rake vendor:prepare
54
+
55
+ - name: Install dependencies
56
+ run: bundle install --jobs 4
57
+
58
+ - name: Compile vendored native extensions
59
+ run: bundle exec rake vendor:compile
60
+
61
+ - name: Run adapter specs
62
+ run: bundle exec rake spec:adapter
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
+ gem "leptris", "~> 1.1"
12
13
  gem "libxml-ruby", "~> 5.0"
13
14
  gem "nokogiri", "~> 1.18"
14
15
  gem "openssl", "~> 3.0"
@@ -30,7 +31,8 @@ gem "rubocop-performance"
30
31
  gem "rubocop-rake"
31
32
  gem "rubocop-rspec"
32
33
  gem "simplecov", require: false
33
- gem "stackprof"
34
+ # stackprof relies on POSIX signals (SIGPROF/sigaction) unavailable on Windows
35
+ gem "stackprof" unless Gem.win_platform?
34
36
  gem "tempfile"
35
37
 
36
38
  # Needed by get_process_mem on Windows
data/README.adoc CHANGED
@@ -47,12 +47,21 @@ LibXML:: https://github.com/xml4r/libxml-ruby[libxml-ruby], Ruby bindings
47
47
  for the performant https://github.com/GNOME/libxml2[libxml2] C library.
48
48
  Alternative to Nokogiri with similar performance characteristics.
49
49
 
50
+ Leptris:: https://github.com/leptris/leptris[Leptris], a pure-C99 XML 1.0
51
+ parser with a W3C-conformant XPath 1.0 engine, consumed through the
52
+ https://github.com/leptris/leptris-ruby[leptris-ruby] FFI binding
53
+ (precompiled platform gems — no compilation at install). Fastest on
54
+ every measured operation; requires leptris >= 1.2 for programmatic
55
+ document construction.
56
+
50
57
  ==== Default adapter selection
51
58
 
52
59
  When no adapter is explicitly specified, Moxml selects one automatically based on
53
60
  the runtime environment:
54
61
 
55
62
  . If running on Opal (`RUBY_ENGINE == "opal"`), Oga is used (pure Ruby, no C extensions)
63
+ . Otherwise, if the installed leptris gem supports programmatic document
64
+ construction (`Leptris::XML::Document.create`, leptris >= 1.2), Leptris is used
56
65
  . Otherwise, Moxml detects already-loaded XML libraries in this order:
57
66
  .. Nokogiri
58
67
  .. Ox
@@ -63,7 +72,7 @@ the runtime environment:
63
72
  ----
64
73
  # Automatic detection — picks the best available adapter
65
74
  ctx = Moxml.new
66
- ctx.config.adapter_name # => :nokogiri (or :oga on Opal)
75
+ ctx.config.adapter_name # => :leptris (leptris >= 1.2), :nokogiri otherwise, :oga on Opal
67
76
 
68
77
  # Explicit override — always takes precedence
69
78
  ctx = Moxml.new(:ox)
@@ -80,9 +89,9 @@ The following table summarizes the features supported by each library.
80
89
  NOTE: The checkmarks indicate support for the feature, while the footnotes
81
90
  provide additional context for specific features.
82
91
 
83
- [cols="1,1,1,1,1,3"]
92
+ [cols="1,1,1,1,1,1,1,3"]
84
93
  |===
85
- |Feature |Nokogiri |Oga |REXML |LibXML |Ox |HeadedOx
94
+ |Feature |Nokogiri |Oga |REXML |LibXML |Ox |HeadedOx |Leptris
86
95
 
87
96
  |Parsing, serializing
88
97
  | ✅
@@ -91,6 +100,7 @@ provide additional context for specific features.
91
100
  | ✅
92
101
  | ✅
93
102
  | ✅
103
+ | ✅
94
104
 
95
105
  |SAX parsing
96
106
  | ✅ Full (10/10 events)
@@ -99,6 +109,7 @@ provide additional context for specific features.
99
109
  | ✅ Full (10/10 events)
100
110
  | ⚠️ Core (4/10 events) See NOTE 7.
101
111
  | ⚠️ Core (4/10 events) See NOTE 7.
112
+ | ✅ Full See NOTE 8.
102
113
 
103
114
  |Node manipulation
104
115
  | ✅
@@ -107,6 +118,7 @@ provide additional context for specific features.
107
118
  | ✅
108
119
  | ✅ See NOTE 1.
109
120
  | ✅ See NOTE 1.
121
+ | ✅
110
122
 
111
123
  |Basic XPath
112
124
  | ✅
@@ -115,6 +127,8 @@ provide additional context for specific features.
115
127
  | ✅
116
128
  | Uses Ox-specific API `locate`. See NOTE 2.
117
129
  | ✅ Full XPath 1.0. See NOTE 3.
130
+ | ✅ Full XPath 1.0 via Moxml's Ruby engine. See NOTE 8.
131
+
118
132
 
119
133
  |XPath with namespaces
120
134
  | ✅
@@ -123,6 +137,7 @@ provide additional context for specific features.
123
137
  | ✅
124
138
  | Uses Ox-specific API `locate`. See NOTE 2.
125
139
  | ⚠️ Basic. See NOTE 3.
140
+ | ✅ Full See NOTE 8.
126
141
 
127
142
  |===
128
143
 
@@ -139,15 +154,23 @@ NOTE: Ox/HeadedOx SAX: Only core events supported (start_element, end_element,
139
154
  characters, errors). No separate CDATA, comment, or processing instruction
140
155
  events.
141
156
 
157
+ NOTE: Leptris: XPath is evaluated by Moxml's pure-Ruby XPath 1.0 engine
158
+ (same as HeadedOx) rather than libleptris's native engine, because the
159
+ native engine does not resolve expression prefixes against
160
+ document-declared namespaces. XML declarations, programmatic DOCTYPEs,
161
+ and entity references are value objects stored via NativeAttachment —
162
+ libleptris has no first-class nodes for them.
163
+
142
164
  == Adapter comparison
143
165
 
144
166
  === Feature compatibility matrix
145
167
 
146
- [cols="3,1,1,1,1,1,1", options="header"]
168
+ [cols="3,1,1,1,1,1,1,1", options="header"]
147
169
  |===
148
- | Feature/Operation | Nokogiri | Oga | REXML | LibXML | Ox | HeadedOx
170
+ | Feature/Operation | Nokogiri | Oga | REXML | LibXML | Ox | HeadedOx | Leptris
149
171
 
150
172
  | *Core Operations*
173
+
151
174
  |
152
175
  |
153
176
  |
@@ -162,6 +185,7 @@ events.
162
185
  | ✅ Full
163
186
  | ✅ Full
164
187
  | ✅ Full
188
+ | ✅ Full
165
189
 
166
190
  | Parse XML file/IO
167
191
  | ✅ Full
@@ -170,6 +194,7 @@ events.
170
194
  | ✅ Full
171
195
  | ✅ Full
172
196
  | ✅ Full
197
+ | ✅ Full
173
198
 
174
199
  | Serialize to XML
175
200
  | ✅ Full
@@ -178,9 +203,12 @@ events.
178
203
  | ✅ Full
179
204
  | ✅ Full
180
205
  | ✅ Full
206
+ | ✅ Full
181
207
 
182
208
  | *Element Operations*
209
+
183
210
  |
211
+
184
212
  |
185
213
  |
186
214
  |
@@ -194,6 +222,7 @@ events.
194
222
  | ✅ Full
195
223
  | ✅ Full
196
224
  | ✅ Full
225
+ | ✅ Full
197
226
 
198
227
  | Get/set attributes
199
228
  | ✅ Full
@@ -202,6 +231,7 @@ events.
202
231
  | ✅ Full
203
232
  | ✅ Full
204
233
  | ✅ Full
234
+ | ✅ Full
205
235
 
206
236
  | Add/remove children
207
237
  | ✅ Full
@@ -210,6 +240,7 @@ events.
210
240
  | ✅ Full
211
241
  | ✅ Full
212
242
  | ✅ Full
243
+ | ✅ Full
213
244
 
214
245
  | Replace nodes
215
246
  | ✅ Full
@@ -218,9 +249,12 @@ events.
218
249
  | ✅ Full
219
250
  | ⚠️ Limited^1^
220
251
  | ⚠️ Limited^1^
252
+ | ✅ Full
221
253
 
222
254
  | *Namespace Operations*
255
+
223
256
  |
257
+
224
258
  |
225
259
  |
226
260
  |
@@ -234,6 +268,7 @@ events.
234
268
  | ✅ Full
235
269
  | ✅ Full
236
270
  | ✅ Full
271
+ | ✅ Full
237
272
 
238
273
  | Default namespaces
239
274
  | ✅ Full
@@ -242,6 +277,7 @@ events.
242
277
  | ✅ Full
243
278
  | ⚠️ Basic
244
279
  | ⚠️ Basic
280
+ | ✅ Full
245
281
 
246
282
  | Namespace inheritance
247
283
  | ✅ Full
@@ -250,6 +286,7 @@ events.
250
286
  | ✅ Full
251
287
  | ❌ None
252
288
  | ❌ None^5^
289
+ | ✅ Full
253
290
 
254
291
  | Namespaced attributes
255
292
  | ✅ Full
@@ -258,9 +295,12 @@ events.
258
295
  | ✅ Full
259
296
  | ⚠️ Limited
260
297
  | ⚠️ Limited^5^
298
+ | ✅ Full
261
299
 
262
300
  | *XPath Queries*
301
+
263
302
  |
303
+
264
304
  |
265
305
  |
266
306
  |
@@ -274,6 +314,7 @@ events.
274
314
  | ✅ Full
275
315
  | ✅ Full
276
316
  | ✅ Full
317
+ | ✅ Full
277
318
 
278
319
  | Attribute predicates (`[@id]`)
279
320
  | ✅ Full
@@ -282,6 +323,7 @@ events.
282
323
  | ✅ Full
283
324
  | ⚠️ Existence only^2^
284
325
  | ✅ Full
326
+ | ✅ Full
285
327
 
286
328
  | Attribute values (`[@id='123']`)
287
329
  | ✅ Full
@@ -290,6 +332,7 @@ events.
290
332
  | ✅ Full
291
333
  | ❌ None^3^
292
334
  | ✅ Full
335
+ | ✅ Full
293
336
 
294
337
  | Logical operators (`[@a and @b]`)
295
338
  | ✅ Full
@@ -298,6 +341,7 @@ events.
298
341
  | ✅ Full
299
342
  | ❌ None
300
343
  | ✅ Full
344
+ | ✅ Full
301
345
 
302
346
  | Position predicates (`[1]`, `[last()]`)
303
347
  | ✅ Full
@@ -306,6 +350,7 @@ events.
306
350
  | ✅ Full
307
351
  | ❌ None
308
352
  | ✅ Full
353
+ | ✅ Full
309
354
 
310
355
  | Text predicates (`[text()='x']`)
311
356
  | ✅ Full
@@ -314,6 +359,7 @@ events.
314
359
  | ✅ Full
315
360
  | ❌ None
316
361
  | ✅ Full
362
+ | ✅ Full
317
363
 
318
364
  | Namespace-aware queries
319
365
  | ✅ Full
@@ -322,6 +368,7 @@ events.
322
368
  | ✅ Full
323
369
  | ❌ None
324
370
  | ⚠️ Basic^5^
371
+ | ✅ Full
325
372
 
326
373
  | Parent axis (`..`)
327
374
  | ✅ Full
@@ -330,6 +377,7 @@ events.
330
377
  | ✅ Full
331
378
  | ❌ None
332
379
  | ✅ Full
380
+ | ✅ Full
333
381
 
334
382
  | Sibling axes
335
383
  | ✅ Full
@@ -338,6 +386,7 @@ events.
338
386
  | ✅ Full
339
387
  | ❌ None
340
388
  | ❌ None^5^
389
+ | ✅ Full
341
390
 
342
391
  | XPath functions (`count()`, etc.)
343
392
  | ✅ Full
@@ -346,9 +395,12 @@ events.
346
395
  | ✅ Full
347
396
  | ❌ None
348
397
  | ✅ All 27
398
+ | ✅ All 27
349
399
 
350
400
  | *Special Content*
401
+
351
402
  |
403
+
352
404
  |
353
405
  |
354
406
  |
@@ -362,6 +414,7 @@ events.
362
414
  | ✅ Full
363
415
  | ✅ Full
364
416
  | ✅ Full
417
+ | ✅ Full
365
418
 
366
419
  | Comments
367
420
  | ✅ Full
@@ -370,6 +423,7 @@ events.
370
423
  | ✅ Full
371
424
  | ✅ Full
372
425
  | ✅ Full
426
+ | ✅ Full
373
427
 
374
428
  | Processing instructions
375
429
  | ✅ Full
@@ -378,6 +432,7 @@ events.
378
432
  | ✅ Full
379
433
  | ✅ Full
380
434
  | ✅ Full
435
+ | ✅ Full
381
436
 
382
437
  | DOCTYPE declarations
383
438
  | ✅ Full
@@ -386,9 +441,12 @@ events.
386
441
  | ✅ Full
387
442
  | ✅ Full
388
443
  | ✅ Full
444
+ | ⚠️ Value-object sidecar^8^
389
445
 
390
446
  | *Performance*
447
+
391
448
  |
449
+
392
450
  |
393
451
  |
394
452
  |
@@ -402,6 +460,7 @@ events.
402
460
  | Fast
403
461
  | Very Fast
404
462
  | Very Fast
463
+ | Fastest
405
464
 
406
465
  | Serialize speed
407
466
  | Fast
@@ -410,6 +469,7 @@ events.
410
469
  | Medium
411
470
  | Very Fast
412
471
  | Very Fast
472
+ | Very Fast
413
473
 
414
474
  | Memory usage
415
475
  | Good
@@ -418,6 +478,7 @@ events.
418
478
  | Good
419
479
  | Excellent
420
480
  | Excellent
481
+ | Excellent
421
482
 
422
483
  | Thread safety
423
484
  | ✅ Yes
@@ -426,13 +487,15 @@ events.
426
487
  | ✅ Yes
427
488
  | ✅ Yes
428
489
  | ✅ Yes
490
+ | ⚠️ One doc per thread^8^
429
491
  |===
430
492
  +
431
493
  ^1^ Ox/HeadedOx: Text node replacement may fail in some cases due to internal node structure +
432
494
  ^2^ Ox: `//book[@id]` works (returns all book elements), but doesn't filter by attribute existence +
433
495
  ^3^ HeadedOx: Full XPath 1.0 with all 27 functions and 6 axes. Pure Ruby XPath engine on Ox's C parser. 99.20% pass rate. See link:docs/headed-ox.adoc[] +
434
496
  ^4^ Ox: Use `.find { |el| el["id"] == "123" }` instead of XPath attribute value predicates +
435
- ^5^ HeadedOx limitations: Namespace introspection and 7 axes not implemented. See link:docs/HEADED_OX_LIMITATIONS.md[]
497
+ ^5^ HeadedOx limitations: Namespace introspection and 7 axes not implemented. See link:docs/HEADED_OX_LIMITATIONS.md[] +
498
+ ^8^ Leptris: XPath runs through Moxml's pure-Ruby XPath 1.0 engine (the native C engine does not resolve expression prefixes against document namespaces); DOCTYPEs and XML declarations are value objects stored via NativeAttachment; libleptris documents are not thread-safe — one document per thread
436
499
 
437
500
  === Adapter selection guide
438
501
 
@@ -24,6 +24,9 @@ require "moxml/namespace"
24
24
  require "moxml/doctype"
25
25
  require "moxml/entity_reference"
26
26
  require "moxml/entity_registry"
27
+ require "moxml/entity"
28
+ require "moxml/entity/reference"
29
+ require "moxml/xml_emitter"
27
30
  require "moxml/adapter"
28
31
  require "moxml/adapter/base"
29
32
  require "moxml/adapter/customized_rexml"
@@ -36,6 +39,8 @@ require "moxml/sax/block_handler"
36
39
  require "moxml/sax/namespace_splitter"
37
40
  require "moxml/adapter/rexml"
38
41
  require "moxml/adapter/customized_oga"
42
+ require "moxml/adapter/customized_oga/entity_decoder"
43
+ require "moxml/adapter/customized_oga/raw_value_override"
39
44
  require "moxml/adapter/customized_oga/xml_declaration"
40
45
  require "moxml/adapter/customized_oga/xml_generator"
41
46
  require "moxml/adapter/oga"
@@ -5,62 +5,29 @@ module Moxml
5
5
  class Base
6
6
  # include XmlUtils
7
7
 
8
- # Entity marker for adapters that resolve entities during parsing.
9
- # U+FFFC (Object Replacement Character) + U+FEFF (BOM) is a two-character
10
- # sentinel chosen because this exact sequence followed by a valid entity
11
- # name pattern is vanishingly unlikely in real XML content.
12
- # Non-standard entities like © are converted to this marker before
13
- # parsing, then restored during serialization.
14
- # Standard XML entities (& < > " ') are NOT converted.
15
- ENTITY_MARKER = "\u{FFFC}\u{FEFF}"
16
- ENTITY_NAME_PATTERN = "[a-zA-Z_][\\w.:-]*"
17
- ENTITY_NAME_RE = /&(#{ENTITY_NAME_PATTERN});/
18
- ENTITY_MARKER_RE = /\u{FFFC}\u{FEFF}(#{ENTITY_NAME_PATTERN});/
19
- SERIALIZED_ENTITY_MARKER_RE = /(#{ENTITY_NAME_PATTERN});/
20
- STANDARD_ENTITIES = %w[amp lt gt quot apos].freeze
8
+ # Entity round-trip pipeline lives in Moxml::Entity; these
9
+ # constants and methods stay on the adapter protocol so
10
+ # adapters and wrappers keep their calling convention.
11
+ ENTITY_MARKER = Entity::MARKER
12
+ ENTITY_NAME_PATTERN = Entity::NAME_PATTERN
13
+ ENTITY_NAME_RE = Entity::NAME_RE
14
+ ENTITY_MARKER_RE = Entity::MARKER_RE
15
+ SERIALIZED_ENTITY_MARKER_RE = Entity::SERIALIZED_MARKER_RE
16
+ STANDARD_ENTITIES = Entity::STANDARD_ENTITIES
21
17
 
22
18
  class << self
23
19
  include XmlUtils
24
20
 
25
- # Replace non-standard entity references with markers before parsing.
26
- # Always returns a UTF-8 encoded string.
27
21
  def preprocess_entities(xml)
28
- return "" if xml.nil?
29
-
30
- str = if xml.encoding == Encoding::BINARY
31
- # Binary strings are assumed to be UTF-8. If the bytes are
32
- # not valid UTF-8, fall back to encoding as UTF-8 with
33
- # replacement to avoid raising on gsub.
34
- dup = xml.dup.force_encoding("UTF-8")
35
- if dup.valid_encoding?
36
- dup
37
- else
38
- xml.dup.encode("UTF-8",
39
- "ASCII-8BIT", invalid: :replace, undef: :replace)
40
- end
41
- elsif xml.encoding == Encoding::UTF_8
42
- xml
43
- else
44
- xml.encode("UTF-8")
45
- end
46
- # Fast path: no `&` means no entity references to mark — skip
47
- # the regex scan and string allocation entirely. The vast
48
- # majority of XML payloads contain no entity references.
49
- return str unless str.include?("&")
50
-
51
- str.gsub(ENTITY_NAME_RE) do |match|
52
- STANDARD_ENTITIES.include?(::Regexp.last_match(1)) ? match : "#{ENTITY_MARKER}#{::Regexp.last_match(1)};"
53
- end
22
+ Entity.preprocess_entities(xml)
54
23
  end
55
24
 
56
- # Restore entity markers back to named entity references.
57
- def restore_entities(text)
58
- return text unless text.is_a?(String)
25
+ def decode_entities(text)
26
+ Entity.decode_entities(text)
27
+ end
59
28
 
60
- # Force UTF-8 encoding since markers are UTF-8 characters
61
- str = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding("UTF-8")
62
- result = str.gsub(ENTITY_MARKER_RE, '&\1;')
63
- result.gsub(SERIALIZED_ENTITY_MARKER_RE, '&\1;')
29
+ def restore_entities(text)
30
+ Entity.restore_entities(text)
64
31
  end
65
32
 
66
33
  def set_root(_doc, _element)
@@ -71,7 +38,7 @@ module Moxml
71
38
  )
72
39
  end
73
40
 
74
- def parse(_xml, _options = {})
41
+ def parse(_xml, _options = {}, _context = nil)
75
42
  raise Moxml::NotImplementedError.new(
76
43
  "parse not implemented",
77
44
  feature: "parse",
@@ -172,12 +139,35 @@ namespace_validation_mode: :strict)
172
139
  create_native_entity_reference(name)
173
140
  end
174
141
 
142
+ # Mutation return contract: protocol methods that may change
143
+ # which native a wrapper tracks (set_attribute_name,
144
+ # set_namespace, set_attribute_value) always return the native
145
+ # the wrapper must keep tracking — the same object when
146
+ # mutated in place, a fresh object when the adapter recreates
147
+ # the node.
175
148
  def set_attribute_name(attribute, name)
176
149
  attribute.name = name
150
+ attribute
151
+ end
152
+
153
+ def set_namespace(_node, _namespace)
154
+ raise Moxml::NotImplementedError.new(
155
+ "set_namespace not implemented",
156
+ feature: "set_namespace",
157
+ adapter: name,
158
+ )
177
159
  end
178
160
 
179
161
  def set_attribute_value(attribute, value)
180
162
  attribute.value = value
163
+ attribute
164
+ end
165
+
166
+ # Remove a specific native attribute node from its owning
167
+ # element. Semantics (which attribute a name addresses) live
168
+ # in Moxml::AttributeResolver; this is the raw primitive.
169
+ def remove_attribute_native(attr)
170
+ attr.remove
181
171
  end
182
172
 
183
173
  def entity_reference_name(node)
@@ -212,6 +202,20 @@ namespace_validation_mode: :strict)
212
202
  # Called when a Declaration node is removed from a document.
213
203
  def remove_declaration(_native_doc); end
214
204
 
205
+ # Source line of a native node (1-based), or nil when the
206
+ # underlying backend does not track source positions.
207
+ # Adapters that track lines (Nokogiri, LibXML) override this.
208
+ def line_number(_node)
209
+ nil
210
+ end
211
+
212
+ # Local name of a native attribute node. Adapters whose natives
213
+ # carry qualified names override this to expose the local part;
214
+ # the wrapper composes the prefix.
215
+ def attribute_name(attr)
216
+ attr.name
217
+ end
218
+
215
219
  # Return the actual native node after an add_child operation.
216
220
  # Override for adapters where node identity may change (e.g., LibXML doc.root=).
217
221
  def actual_native(child_native, _parent_native)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Adapter
5
+ module CustomizedLeptris
6
+ # Wrapper for an XML declaration. libleptris stores declaration
7
+ # data on the document struct (read-only, serialized via options)
8
+ # and has no first-class declaration node, so Moxml exposes this
9
+ # mutable value object instead.
10
+ class Declaration
11
+ attr_accessor :version, :encoding, :standalone, :parent_doc
12
+
13
+ def initialize(version = "1.0", encoding = "UTF-8", standalone = nil)
14
+ @version = version
15
+ @encoding = encoding
16
+ @standalone = standalone
17
+ end
18
+
19
+ def to_xml
20
+ XmlEmitter.declaration_xml(version, encoding, standalone)
21
+ end
22
+
23
+ def ==(other)
24
+ other.is_a?(self.class) &&
25
+ version == other.version &&
26
+ encoding == other.encoding &&
27
+ standalone == other.standalone
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Adapter
5
+ module CustomizedLeptris
6
+ # Wrapper for a programmatic DOCTYPE. libleptris only parses
7
+ # DOCTYPEs from source; it has no API to create one, so Moxml
8
+ # stores this value object in the document's attachments.
9
+ class Doctype
10
+ attr_accessor :name, :external_id, :system_id, :parent_doc
11
+
12
+ def initialize(name, external_id = nil, system_id = nil)
13
+ @name = name
14
+ @external_id = external_id
15
+ @system_id = system_id
16
+ end
17
+
18
+ def to_xml
19
+ XmlEmitter.doctype_xml(name, external_id, system_id)
20
+ end
21
+
22
+ def ==(other)
23
+ other.is_a?(self.class) &&
24
+ name == other.name &&
25
+ external_id == other.external_id &&
26
+ system_id == other.system_id
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Adapter
5
+ module CustomizedLeptris
6
+ # libleptris expands the five built-in entities at parse time
7
+ # and has no custom ones; the adapter carries entity references
8
+ # as Moxml::Entity::Reference values over marker-bearing text.
9
+ EntityReference = ::Moxml::Entity::Reference
10
+ end
11
+ end
12
+ end