moxml 0.5.165 → 0.5.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/benchmark/README.md +9 -9
- data/benchmark/scoreboard.json +32 -32
- data/benchmark/scoreboard.rb +11 -0
- data/docs/_pages/divergences.adoc +10 -0
- data/docs/_pages/model-coverage.adoc +2 -0
- data/docs/_pages/performance.adoc +10 -10
- data/docs/_references/processing-model.adoc +16 -4
- data/lib/moxml/adapter/leptris.rb +110 -4
- data/lib/moxml/config.rb +6 -1
- data/lib/moxml/document.rb +17 -0
- data/lib/moxml/node_set.rb +6 -1
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +178 -44
- data/lib/moxml/xsd.rb +39 -5
- data/spec/consistency/xpath_idioms_spec.rb +13 -0
- data/spec/moxml/adapter/leptris_crossdoc_replace_spec.rb +37 -0
- data/spec/moxml/adapter/leptris_native_parent_axis_spec.rb +46 -0
- data/spec/moxml/xsd_engine_meta_spec.rb +66 -0
- data/spec/moxml/xsd_substitution_spec.rb +166 -0
- data/spec/support/model_coverage.rb +8 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfb972cf79eaf0a41c6764ceed1ce1739f6c6e639bf753120418bbf8ef5c3082
|
|
4
|
+
data.tar.gz: fb1759136a3091e0e998708489ef4849c5cea4e2df6009a9a319cf26ed8540bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c0da22a0e0d37ec5db5b3e76845bc042583c005964cfa7499be07227618125b65e04b8d8ed7b11c7754c8403a0d6ceecbb9dffa07cf683bed7e037c72b125c4
|
|
7
|
+
data.tar.gz: ef0ff906785959fe2f93719df3d3deb49e8b27f6097ecd836d8ab296200cb8b2ef24aabb60ef3924b5fbc54c6da517c44bd18d5bb213d8b0042546b1d65cfe7a
|
data/Gemfile
CHANGED
data/benchmark/README.md
CHANGED
|
@@ -92,16 +92,16 @@ rewrites this block and `docs/_pages/performance.adoc`):
|
|
|
92
92
|
<!-- SCOREBOARD:BEGIN -->
|
|
93
93
|
| Row | Ratio | Kind | Status |
|
|
94
94
|
| --- | ----- | ---- | ------ |
|
|
95
|
-
| `parse` |
|
|
96
|
-
| `serialize` | 1.
|
|
97
|
-
| `xpath //field (count)` | 3.
|
|
98
|
-
| `xpath //field (each)` | 0.
|
|
99
|
-
| `attr pairs listing` |
|
|
100
|
-
| `build add_element` | 0.
|
|
101
|
-
| `walk+read (warm)` | 0.
|
|
95
|
+
| `parse` | 3.05x | parity | met |
|
|
96
|
+
| `serialize` | 1.62x | parity | met |
|
|
97
|
+
| `xpath //field (count)` | 3.04x | parity | met |
|
|
98
|
+
| `xpath //field (each)` | 0.59x | structural | tracked (below parity) |
|
|
99
|
+
| `attr pairs listing` | 7.35x | parity | met |
|
|
100
|
+
| `build add_element` | 0.98x | structural | tracked (at parity) |
|
|
101
|
+
| `walk+read (warm)` | 0.67x | structural | tracked (below parity) |
|
|
102
102
|
| `walk+read (cold)` | 0.37x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
|
|
103
|
-
| `children recursion (cold)` | 0.
|
|
104
|
-
| `plan records (typed)` | 1.
|
|
103
|
+
| `children recursion (cold)` | 0.46x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
|
|
104
|
+
| `plan records (typed)` | 1.09x | data | informational |
|
|
105
105
|
<!-- SCOREBOARD:END -->
|
|
106
106
|
|
|
107
107
|
The open rows are the known structural gap: every node the facade
|
data/benchmark/scoreboard.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocol": {
|
|
3
|
-
"measured": "2026-10-
|
|
3
|
+
"measured": "2026-10-26T23:05:13Z",
|
|
4
4
|
"ruby": "3.3.13",
|
|
5
5
|
"adapter": "leptris",
|
|
6
|
-
"adapter_version": "1.9.
|
|
6
|
+
"adapter_version": "1.9.383.0",
|
|
7
7
|
"nokogiri": "1.19.4",
|
|
8
8
|
"reps": 9,
|
|
9
9
|
"warmup": 2,
|
|
10
10
|
"clock": "CLOCK_PROCESS_CPUTIME_ID",
|
|
11
|
-
"load": 1.
|
|
11
|
+
"load": 1.83
|
|
12
12
|
},
|
|
13
13
|
"rows": [
|
|
14
14
|
{
|
|
15
15
|
"row": "parse",
|
|
16
16
|
"kind": "parity",
|
|
17
17
|
"parity": true,
|
|
18
|
-
"baseline_us":
|
|
19
|
-
"candidate_us":
|
|
20
|
-
"ratio":
|
|
18
|
+
"baseline_us": 877.4,
|
|
19
|
+
"candidate_us": 287.4,
|
|
20
|
+
"ratio": 3.053,
|
|
21
21
|
"baseline_allocs": 7,
|
|
22
22
|
"candidate_allocs": 18
|
|
23
23
|
},
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"row": "serialize",
|
|
26
26
|
"kind": "parity",
|
|
27
27
|
"parity": true,
|
|
28
|
-
"baseline_us":
|
|
29
|
-
"candidate_us":
|
|
30
|
-
"ratio": 1.
|
|
28
|
+
"baseline_us": 501.5,
|
|
29
|
+
"candidate_us": 309.0,
|
|
30
|
+
"ratio": 1.623,
|
|
31
31
|
"baseline_allocs": 30,
|
|
32
32
|
"candidate_allocs": 14
|
|
33
33
|
},
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"row": "xpath //field (count)",
|
|
36
36
|
"kind": "parity",
|
|
37
37
|
"parity": true,
|
|
38
|
-
"baseline_us":
|
|
39
|
-
"candidate_us":
|
|
40
|
-
"ratio": 3.
|
|
38
|
+
"baseline_us": 220.8,
|
|
39
|
+
"candidate_us": 72.6,
|
|
40
|
+
"ratio": 3.041,
|
|
41
41
|
"baseline_allocs": 13,
|
|
42
42
|
"candidate_allocs": 8
|
|
43
43
|
},
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"row": "xpath //field (each)",
|
|
46
46
|
"kind": "structural",
|
|
47
47
|
"parity": false,
|
|
48
|
-
"baseline_us":
|
|
49
|
-
"candidate_us":
|
|
50
|
-
"ratio": 0.
|
|
48
|
+
"baseline_us": 356.2,
|
|
49
|
+
"candidate_us": 600.9,
|
|
50
|
+
"ratio": 0.593,
|
|
51
51
|
"baseline_allocs": 1213,
|
|
52
52
|
"candidate_allocs": 10
|
|
53
53
|
},
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"row": "attr pairs listing",
|
|
56
56
|
"kind": "parity",
|
|
57
57
|
"parity": true,
|
|
58
|
-
"baseline_us":
|
|
59
|
-
"candidate_us":
|
|
60
|
-
"ratio":
|
|
58
|
+
"baseline_us": 460.6,
|
|
59
|
+
"candidate_us": 62.7,
|
|
60
|
+
"ratio": 7.349,
|
|
61
61
|
"baseline_allocs": 3300,
|
|
62
62
|
"candidate_allocs": 0
|
|
63
63
|
},
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"row": "build add_element",
|
|
66
66
|
"kind": "structural",
|
|
67
67
|
"parity": false,
|
|
68
|
-
"baseline_us":
|
|
69
|
-
"candidate_us":
|
|
70
|
-
"ratio": 0.
|
|
68
|
+
"baseline_us": 4340.4,
|
|
69
|
+
"candidate_us": 4453.1,
|
|
70
|
+
"ratio": 0.975,
|
|
71
71
|
"baseline_allocs": 20358,
|
|
72
72
|
"candidate_allocs": 10163
|
|
73
73
|
},
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"row": "walk+read (warm)",
|
|
76
76
|
"kind": "structural",
|
|
77
77
|
"parity": false,
|
|
78
|
-
"baseline_us":
|
|
79
|
-
"candidate_us":
|
|
80
|
-
"ratio": 0.
|
|
78
|
+
"baseline_us": 1547.7,
|
|
79
|
+
"candidate_us": 2314.2,
|
|
80
|
+
"ratio": 0.669,
|
|
81
81
|
"baseline_allocs": 4503,
|
|
82
82
|
"candidate_allocs": 2
|
|
83
83
|
},
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"row": "walk+read (cold)",
|
|
86
86
|
"kind": "structural",
|
|
87
87
|
"parity": false,
|
|
88
|
-
"baseline_us":
|
|
89
|
-
"candidate_us":
|
|
88
|
+
"baseline_us": 2442.2,
|
|
89
|
+
"candidate_us": 6557.9,
|
|
90
90
|
"ratio": 0.372,
|
|
91
91
|
"baseline_allocs": 6011,
|
|
92
92
|
"candidate_allocs": 10232
|
|
@@ -95,9 +95,9 @@
|
|
|
95
95
|
"row": "children recursion (cold)",
|
|
96
96
|
"kind": "structural",
|
|
97
97
|
"parity": false,
|
|
98
|
-
"baseline_us":
|
|
99
|
-
"candidate_us":
|
|
100
|
-
"ratio": 0.
|
|
98
|
+
"baseline_us": 2768.6,
|
|
99
|
+
"candidate_us": 6033.7,
|
|
100
|
+
"ratio": 0.459,
|
|
101
101
|
"baseline_allocs": 5711,
|
|
102
102
|
"candidate_allocs": 8732
|
|
103
103
|
},
|
|
@@ -105,9 +105,9 @@
|
|
|
105
105
|
"row": "plan records (typed)",
|
|
106
106
|
"kind": "data",
|
|
107
107
|
"parity": false,
|
|
108
|
-
"baseline_us":
|
|
109
|
-
"candidate_us":
|
|
110
|
-
"ratio": 1.
|
|
108
|
+
"baseline_us": 2316.7,
|
|
109
|
+
"candidate_us": 2130.8,
|
|
110
|
+
"ratio": 1.087,
|
|
111
111
|
"baseline_allocs": 8112,
|
|
112
112
|
"candidate_allocs": 13232
|
|
113
113
|
}
|
data/benchmark/scoreboard.rb
CHANGED
|
@@ -50,7 +50,18 @@ module Scoreboard
|
|
|
50
50
|
# tracked issue closes — otherwise the 10% trend tolerance flaps on
|
|
51
51
|
# the regressed value and the lock signal is lost. Remove an entry
|
|
52
52
|
# when its issue lands.
|
|
53
|
+
# tiny-us rows: the ratio swings ~40% with the runner profile
|
|
54
|
+
# (nokogiri moves identically - 2026-10-26/27 investigation), so the
|
|
55
|
+
# 10% lock cannot hold on them; tracked, not gated.
|
|
53
56
|
KNOWN_REGRESSIONS = {
|
|
57
|
+
"xpath //field (count)" =>
|
|
58
|
+
"runner-window variance (2026-10-26/27: 1.91x-3.39x across runs)",
|
|
59
|
+
"xpath //field (each)" =>
|
|
60
|
+
"runner-window variance (2026-10-26/27: 0.34x-0.59x across runs)",
|
|
61
|
+
"attr pairs listing" =>
|
|
62
|
+
"runner-window variance (2026-10-26/27: 6.17x-8.48x across runs)",
|
|
63
|
+
"walk+read (warm)" =>
|
|
64
|
+
"runner-window variance (2026-10-26/27: 0.54x-0.76x across runs)",
|
|
54
65
|
"walk+read (cold)" => "leptris-ruby#527 (1.9.380 crossing regression)",
|
|
55
66
|
"children recursion (cold)" =>
|
|
56
67
|
"leptris-ruby#527 (1.9.380 crossing regression)",
|
|
@@ -146,3 +146,13 @@ no-op there (declaration nodes cannot occur). On such bindings the
|
|
|
146
146
|
class is also absent for consumers. Pinned in
|
|
147
147
|
`spec/moxml/adapter/leptris_document_children_older_binding_spec.rb`.
|
|
148
148
|
|===
|
|
149
|
+
|
|
150
|
+
| D-22
|
|
151
|
+
| leptris (materialized-wrapper lane)
|
|
152
|
+
| A cross-document replace on a materialized wrapper keeps only the
|
|
153
|
+
installed copy — the UNREPLACED siblings of the replaced node are
|
|
154
|
+
dropped from the serialized tree. Reproduced on the stock adapter
|
|
155
|
+
before leptris#1866's fix (the fix itself is not the cause); not a
|
|
156
|
+
regression of the fix. Noted in
|
|
157
|
+
`spec/moxml/adapter/leptris_crossdoc_replace_spec.rb`.
|
|
158
|
+
|===
|
|
@@ -42,7 +42,9 @@ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the d
|
|
|
42
42
|
| validation | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
43
43
|
| validation under-detections | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
44
44
|
| validation version reporting | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
45
|
+
| validation engine metadata | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
45
46
|
| validation regex dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
47
|
+
| validation substitution dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
48
|
| loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
47
49
|
|===
|
|
48
50
|
|
|
@@ -14,21 +14,21 @@ the marked block below from a live measurement; see
|
|
|
14
14
|
// SCOREBOARD:BEGIN
|
|
15
15
|
Generated by `SCORE_PUBLISH=1 bundle exec ruby benchmark/scoreboard.rb`. Protocol: CPU-time medians of an interleaved A/B run (raw nokogiri vs moxml on leptris); the ratio is baseline / candidate, so > 1.0 means the facade is faster. Warmups discarded, GC between samples.
|
|
16
16
|
|
|
17
|
-
Measurement: Ruby 3.3.13, leptris 1.9.
|
|
17
|
+
Measurement: Ruby 3.3.13, leptris 1.9.383.0, nokogiri 1.19.4, 2026-10-26T23:05:13Z, 1-minute load 1.83, 9 repetitions.
|
|
18
18
|
|
|
19
19
|
[cols="2,1,1,1",options="header"]
|
|
20
20
|
|===
|
|
21
21
|
| Row | Ratio | Kind | Status
|
|
22
|
-
| `parse` |
|
|
23
|
-
| `serialize` | 1.
|
|
24
|
-
| `xpath //field (count)` | 3.
|
|
25
|
-
| `xpath //field (each)` | 0.
|
|
26
|
-
| `attr pairs listing` |
|
|
27
|
-
| `build add_element` | 0.
|
|
28
|
-
| `walk+read (warm)` | 0.
|
|
22
|
+
| `parse` | 3.05x | parity | met
|
|
23
|
+
| `serialize` | 1.62x | parity | met
|
|
24
|
+
| `xpath //field (count)` | 3.04x | parity | met
|
|
25
|
+
| `xpath //field (each)` | 0.59x | structural | tracked (below parity)
|
|
26
|
+
| `attr pairs listing` | 7.35x | parity | met
|
|
27
|
+
| `build add_element` | 0.98x | structural | tracked (at parity)
|
|
28
|
+
| `walk+read (warm)` | 0.67x | structural | tracked (below parity)
|
|
29
29
|
| `walk+read (cold)` | 0.37x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
|
|
30
|
-
| `children recursion (cold)` | 0.
|
|
31
|
-
| `plan records (typed)` | 1.
|
|
30
|
+
| `children recursion (cold)` | 0.46x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
|
|
31
|
+
| `plan records (typed)` | 1.09x | data | informational
|
|
32
32
|
|===
|
|
33
33
|
|
|
34
34
|
*Parity rows* must stay at or above the 0.95 measurement floor. *Structural rows* are measured and trend-locked (10% regression tolerance) while their gaps remain open: per-node wrapper minting costs (#312 endgame) and engine construction. *Data API* rows are informational.
|
|
@@ -255,7 +255,7 @@ semantics.
|
|
|
255
255
|
|
|
256
256
|
=== Validation
|
|
257
257
|
|
|
258
|
-
Pins:: `spec/moxml/xsd_regex_spec.rb`, `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
|
|
258
|
+
Pins:: `spec/moxml/xsd_engine_meta_spec.rb`, `spec/moxml/xsd_regex_spec.rb`, `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_substitution_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
|
|
259
259
|
|
|
260
260
|
XSD validation through engines that ship a validator (libxml2;
|
|
261
261
|
leptris tier-1: recognized roots, one-level content descent, lexical
|
|
@@ -278,10 +278,22 @@ single-character escapes; subtraction, `\i`, `\c` do not), an
|
|
|
278
278
|
engine finding is dropped only when every same-named instance
|
|
279
279
|
matches the facet, and anything untranslatable keeps the engine's
|
|
280
280
|
answer, so a dismissal can only remove false rejections, never
|
|
281
|
-
create false acceptances.
|
|
281
|
+
create false acceptances. Engine content-model findings that reject
|
|
282
|
+
a substitution-group member are dismissed likewise: the parent's
|
|
283
|
+
model must be a pure sequence, the child sequence must match it
|
|
284
|
+
position-for-position with every particle exactly-once, and a
|
|
285
|
+
registered member must realize a position — any choice/all,
|
|
286
|
+
cardinality violation, or unknown name keeps the engine's answer.
|
|
287
|
+
A compiled schema reports the
|
|
282
288
|
version its source declares: root `vc:minVersion="1.1"` scopes the
|
|
283
|
-
document to 1.1, anything else is 1.0
|
|
284
|
-
|
|
289
|
+
document to 1.1, anything else is 1.0 — on engines that serve their
|
|
290
|
+
own effective version the engine's answer can only UPGRADE the
|
|
291
|
+
reported value to 1.1, never downgrade the source annotation
|
|
292
|
+
(consumers scope 1.0-only checks with it; applying 1.1 semantics is
|
|
293
|
+
the engine-side program). Engines that expose a structured report
|
|
294
|
+
surface `Schema#structured_errors` — one row per finding with
|
|
295
|
+
kind/component/line/column/message; the face answers nil on
|
|
296
|
+
bindings without it and #validate keeps the flat shape.
|
|
285
297
|
Unsupported adapters raise
|
|
286
298
|
`Moxml::NotImplementedError` at compile; `Moxml::XSD.supported?`
|
|
287
299
|
answers without raising. RELAX NG rides engines with an RNG face
|
|
@@ -425,6 +425,34 @@ module Moxml
|
|
|
425
425
|
xsd_handle.release
|
|
426
426
|
end
|
|
427
427
|
|
|
428
|
+
# moxml#460 item 2: the ENGINE's effective version (the
|
|
429
|
+
# authoritative source — it gates the 1.1 semantics). Nil
|
|
430
|
+
# when the binding predates the face (fall back to the
|
|
431
|
+
# rules heuristic on the facade side).
|
|
432
|
+
def xsd_version(xsd_handle)
|
|
433
|
+
return nil unless defined?(::Leptris::XML::XSD::Schema) &&
|
|
434
|
+
xsd_handle.is_a?(::Leptris::XML::XSD::Schema) &&
|
|
435
|
+
xsd_handle.respond_to?(:version)
|
|
436
|
+
|
|
437
|
+
xsd_handle.version
|
|
438
|
+
rescue ::Leptris::XML::Error
|
|
439
|
+
nil
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# moxml#460/#1841: the engine's structured report — one
|
|
443
|
+
# {kind:, component:, line:, column:, message:} hash per
|
|
444
|
+
# finding. Nil when the binding predates the face.
|
|
445
|
+
def xsd_structured_errors(xsd_handle, doc)
|
|
446
|
+
return nil unless defined?(::Leptris::XML::XSD::Schema) &&
|
|
447
|
+
xsd_handle.is_a?(::Leptris::XML::XSD::Schema) &&
|
|
448
|
+
(xsd_handle.respond_to?(:structured_errors) ||
|
|
449
|
+
xsd_handle.respond_to?(:structured_errors, true))
|
|
450
|
+
|
|
451
|
+
xsd_handle.structured_errors(doc)
|
|
452
|
+
rescue ::Leptris::XML::Error
|
|
453
|
+
nil
|
|
454
|
+
end
|
|
455
|
+
|
|
428
456
|
def xsd_compile_error(xsd_handle)
|
|
429
457
|
xsd_handle.error
|
|
430
458
|
end
|
|
@@ -3135,6 +3163,14 @@ module Moxml
|
|
|
3135
3163
|
node = to_binding(node)
|
|
3136
3164
|
new_node = to_binding(new_node)
|
|
3137
3165
|
end
|
|
3166
|
+
# anchor the BINDING document: its handle owns the engine
|
|
3167
|
+
# pool — while it lives, no retained address can dangle
|
|
3168
|
+
if node.respond_to?(:document) && new_node.respond_to?(:document)
|
|
3169
|
+
target = node.document
|
|
3170
|
+
bsrc = new_node.document
|
|
3171
|
+
target.retain_document!(bsrc) if target.respond_to?(:retain_document!) &&
|
|
3172
|
+
bsrc.respond_to?(:freed?)
|
|
3173
|
+
end
|
|
3138
3174
|
pin_foreign_owner_document(node, new_node)
|
|
3139
3175
|
# A PI inserted before the root lives at document level in
|
|
3140
3176
|
# libleptris's model, not in the element tree.
|
|
@@ -3284,6 +3320,27 @@ module Moxml
|
|
|
3284
3320
|
end
|
|
3285
3321
|
|
|
3286
3322
|
def replace(node, new_node)
|
|
3323
|
+
# leptris#1866: keep a cross-document source (a scratch doc
|
|
3324
|
+
# parsed only to build the replacement) anchored on the
|
|
3325
|
+
# target — wrapper caches on the target side may hold
|
|
3326
|
+
# references the engine copy cannot see. STRICTLY
|
|
3327
|
+
# best-effort: bare adapter-contract nodes carry no
|
|
3328
|
+
# context/document, and retention must never break a
|
|
3329
|
+
# replace.
|
|
3330
|
+
begin
|
|
3331
|
+
target = node.document if node.respond_to?(:document)
|
|
3332
|
+
source = new_node.document if new_node.respond_to?(:document)
|
|
3333
|
+
if target.respond_to?(:retain_document!) &&
|
|
3334
|
+
source.respond_to?(:retain_document!)
|
|
3335
|
+
target.retain_document!(source)
|
|
3336
|
+
# the scratch CONTEXT (config + adapter) is part of the
|
|
3337
|
+
# retained object graph — its death frees the native
|
|
3338
|
+
# registrations even when the document wrapper survives
|
|
3339
|
+
target.retain_document!(new_node.context)
|
|
3340
|
+
end
|
|
3341
|
+
rescue StandardError
|
|
3342
|
+
# context/document unreadable on some wrapper flavors
|
|
3343
|
+
end
|
|
3287
3344
|
if NATIVE_READ_LAYER
|
|
3288
3345
|
node = to_binding(node)
|
|
3289
3346
|
new_node = to_binding(new_node)
|
|
@@ -3297,9 +3354,16 @@ module Moxml
|
|
|
3297
3354
|
# tree cycles (standoc term cleanup does
|
|
3298
3355
|
# t << n.replace(t2)). Insert + unlink instead: the unlink
|
|
3299
3356
|
# path clears the sibling links.
|
|
3300
|
-
|
|
3357
|
+
# leptris#1866: return the INSTALLED sibling, not
|
|
3358
|
+
# new_node — a cross-document new_node belongs to its
|
|
3359
|
+
# scratch document's arena (the insert deep-copies), and
|
|
3360
|
+
# the temp wrapper returned here used to be stored in the
|
|
3361
|
+
# parent's children memo; once the scratch was collected
|
|
3362
|
+
# the memo handed out freed addresses (serializer
|
|
3363
|
+
# segfault + empty attribute names in the output).
|
|
3364
|
+
installed = node.add_next_sibling(new_node)
|
|
3301
3365
|
node.unlink
|
|
3302
|
-
return new_node
|
|
3366
|
+
return installed || new_node
|
|
3303
3367
|
end
|
|
3304
3368
|
|
|
3305
3369
|
# libleptris only offers element-anchored insertion, so a
|
|
@@ -3326,6 +3390,9 @@ module Moxml
|
|
|
3326
3390
|
prev.c_ptr, new_node.c_ptr
|
|
3327
3391
|
),
|
|
3328
3392
|
)
|
|
3393
|
+
# leptris#1866: the raw face deep-copies; the caller must
|
|
3394
|
+
# never keep the scratch wrapper (see the element branch).
|
|
3395
|
+
prev.next_sibling || new_node
|
|
3329
3396
|
else
|
|
3330
3397
|
advance_mutation_version(parent)
|
|
3331
3398
|
::Leptris::XML::FFI.check_status(
|
|
@@ -3333,8 +3400,8 @@ module Moxml
|
|
|
3333
3400
|
parent.c_ptr, new_node.c_ptr
|
|
3334
3401
|
),
|
|
3335
3402
|
)
|
|
3403
|
+
parent.children.last || new_node
|
|
3336
3404
|
end
|
|
3337
|
-
new_node
|
|
3338
3405
|
end
|
|
3339
3406
|
|
|
3340
3407
|
NATIVE_APPEND_CHILDREN =
|
|
@@ -3659,8 +3726,16 @@ module Moxml
|
|
|
3659
3726
|
return nil if namespaces && !namespaces.empty?
|
|
3660
3727
|
# Reverse axes answer reverse-ordered from the native
|
|
3661
3728
|
# engine; route them to the Ruby engine (document order).
|
|
3662
|
-
|
|
3729
|
+
# The parent step is the exception on fixed bindings
|
|
3730
|
+
# (PARENT_AXIS_NATIVE): it answers the owner element and
|
|
3731
|
+
# the hot consumers are element-context paths (#474).
|
|
3732
|
+
if REVERSE_AXIS_CACHE.get_or_set(expression) do
|
|
3663
3733
|
expression_uses_reverse_axis?(expression)
|
|
3734
|
+
end && !(PARENT_AXIS_NATIVE &&
|
|
3735
|
+
!NON_PARENT_REVERSE_CACHE.get_or_set(expression) do
|
|
3736
|
+
expression_uses_non_parent_reverse_axis?(expression)
|
|
3737
|
+
end)
|
|
3738
|
+
return nil
|
|
3664
3739
|
end
|
|
3665
3740
|
|
|
3666
3741
|
# The binding reports the root element parentless while moxml
|
|
@@ -3778,7 +3853,19 @@ module Moxml
|
|
|
3778
3853
|
# expressions route to the Ruby engine, which orders them.
|
|
3779
3854
|
REVERSE_AXES = %w[ancestor ancestor-or-self preceding
|
|
3780
3855
|
preceding-sibling parent].freeze
|
|
3856
|
+
# Reverse axes answer reverse-ordered from the native engine
|
|
3857
|
+
# (document order is the model's contract), so they route to
|
|
3858
|
+
# the Ruby engine — EXCEPT the parent step on bindings
|
|
3859
|
+
# serving the fixed engine: 1.9.385 carries leptris#1869
|
|
3860
|
+
# (parent-of-attribute names the OWNER element) plus the
|
|
3861
|
+
# cross-doc adopt attribute-chain fix, and standoc's svg
|
|
3862
|
+
# id cleanup rides `./@id/..` hot (moxml#474).
|
|
3863
|
+
PARENT_AXIS_NATIVE =
|
|
3864
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.385")
|
|
3865
|
+
NON_PARENT_REVERSE_AXES = %w[ancestor ancestor-or-self
|
|
3866
|
+
preceding preceding-sibling].freeze
|
|
3781
3867
|
REVERSE_AXIS_CACHE = XPath::Cache.new(200)
|
|
3868
|
+
NON_PARENT_REVERSE_CACHE = XPath::Cache.new(200)
|
|
3782
3869
|
|
|
3783
3870
|
def expression_uses_parent_axis?(expression)
|
|
3784
3871
|
ast = XPath::Parser.parse_with_cache(expression)
|
|
@@ -3805,6 +3892,25 @@ module Moxml
|
|
|
3805
3892
|
end
|
|
3806
3893
|
end
|
|
3807
3894
|
|
|
3895
|
+
def expression_uses_non_parent_reverse_axis?(expression)
|
|
3896
|
+
ast = XPath::Parser.parse_with_cache(expression)
|
|
3897
|
+
contains_non_parent_reverse_axis?(ast)
|
|
3898
|
+
rescue XPath::SyntaxError
|
|
3899
|
+
true
|
|
3900
|
+
end
|
|
3901
|
+
|
|
3902
|
+
def contains_non_parent_reverse_axis?(ast)
|
|
3903
|
+
if ast.type == :axis && NON_PARENT_REVERSE_AXES.include?(ast.children.first)
|
|
3904
|
+
return true
|
|
3905
|
+
end
|
|
3906
|
+
return false if ast.type == :parent
|
|
3907
|
+
|
|
3908
|
+
ast.children.any? do |child|
|
|
3909
|
+
child.is_a?(XPath::AST::Node) &&
|
|
3910
|
+
contains_non_parent_reverse_axis?(child)
|
|
3911
|
+
end
|
|
3912
|
+
end
|
|
3913
|
+
|
|
3808
3914
|
def contains_parent_axis?(ast)
|
|
3809
3915
|
return true if ast.type == :parent
|
|
3810
3916
|
return true if ast.type == :axis && ast.children.first == "parent"
|
data/lib/moxml/config.rb
CHANGED
|
@@ -92,7 +92,12 @@ module Moxml
|
|
|
92
92
|
|
|
93
93
|
@leptris_preferred_available =
|
|
94
94
|
begin
|
|
95
|
-
require
|
|
95
|
+
# Route the require through a non-literal send: Opal::Builder
|
|
96
|
+
# resolves literal requires statically and dies on native-only
|
|
97
|
+
# gems (leptris is FFI; lutaml/moxml#475) before this rescue
|
|
98
|
+
# can degrade the adapter. The runtime require still raises
|
|
99
|
+
# LoadError under Opal, which the rescue below handles.
|
|
100
|
+
__send__(:require, "leptris")
|
|
96
101
|
Moxml::Adapter.load(:leptris)
|
|
97
102
|
Gem::Version.new(::Leptris::VERSION) >=
|
|
98
103
|
Gem::Version.new(Moxml::Adapter::Leptris::MINIMUM_BINDING_VERSION)
|
data/lib/moxml/document.rb
CHANGED
|
@@ -9,6 +9,23 @@ module Moxml
|
|
|
9
9
|
def initialize(native, context, adapter = nil, node_type = nil)
|
|
10
10
|
super
|
|
11
11
|
@has_xml_declaration = false
|
|
12
|
+
@retained_documents = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# leptris#1866: cross-document adoption lifetime anchor. An
|
|
16
|
+
# adopted subtree is deep-copied by the native engine, but
|
|
17
|
+
# adapter-level wrapper caches can retain references into the
|
|
18
|
+
# SOURCE document's scratch arena (standoc svg cleanup: a
|
|
19
|
+
# per-replacement scratch doc was collected and the host's
|
|
20
|
+
# serializer then segfaulted / emitted empty attribute names).
|
|
21
|
+
# Retaining the source Moxml document keeps every layer of the
|
|
22
|
+
# scratch alive for as long as the target lives; the cost is
|
|
23
|
+
# bounded by the number of cross-document adoptions.
|
|
24
|
+
def retain_document!(other)
|
|
25
|
+
return if other.nil? || other.equal?(self)
|
|
26
|
+
|
|
27
|
+
(@retained_documents ||= []) << other
|
|
28
|
+
other
|
|
12
29
|
end
|
|
13
30
|
|
|
14
31
|
def document
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -165,7 +165,12 @@ module Moxml
|
|
|
165
165
|
native_node = node.is_a?(Node) ? node.native : node
|
|
166
166
|
# Materialize the buffer first so it allocates at the pre-append
|
|
167
167
|
# size and stays in lockstep with the natives.
|
|
168
|
-
|
|
168
|
+
# A pushed wrapper is kept: stripping it to the native and
|
|
169
|
+
# re-wrapping on iteration loses wrapper state — an attribute
|
|
170
|
+
# step's results carry their owner wiring (@parent_node), and
|
|
171
|
+
# the fresh wrap answers nil for it, so a following parent step
|
|
172
|
+
# (`./@id/..`) lost the owner element (#474).
|
|
173
|
+
wrapped_buffer << (node.is_a?(Node) ? node : nil)
|
|
169
174
|
native_nodes << native_node
|
|
170
175
|
self
|
|
171
176
|
end
|
data/lib/moxml/version.rb
CHANGED