moxml 0.5.166 → 0.5.168
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 +17 -4
- data/lib/moxml/adapter/base.rb +0 -5
- data/lib/moxml/adapter/leptris.rb +121 -13
- 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/xpath/ordering.rb +11 -0
- data/lib/moxml/xsd/builtin_lexical.rb +251 -0
- data/lib/moxml/xsd.rb +46 -11
- data/spec/consistency/xpath_idioms_spec.rb +41 -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_builtin_lexical_spec.rb +139 -0
- data/spec/moxml/xsd_engine_meta_spec.rb +66 -0
- data/spec/support/model_coverage.rb +8 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b55c7bbd0cbaa945ab4404c9fe40b354ae5b291672804f874b33318abd26dc3
|
|
4
|
+
data.tar.gz: 33ac60d77afebe105bf6ccfcd4f07482f1b33c42cf573c6e169a2b2aacc0b2df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33d2df43783702c43372335cfe88fb0449cb04984889f3932fcd085592c26f33ad68303c0d4019a6ed01831a325bd1070d2d261a47cfc604867b9b221452a69a
|
|
7
|
+
data.tar.gz: 5417a78c416bbbc737a13e9c369f7a7064e5946f44da6e80dad795fee279c1d355f86d10dd5f95bfcbe61b44eba1a7c7d3f6cd462197b7a00698a1f74f81fa35
|
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 builtin lexical table | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
48
|
| validation substitution dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
47
49
|
| loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
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,11 +255,18 @@ semantics.
|
|
|
255
255
|
|
|
256
256
|
=== Validation
|
|
257
257
|
|
|
258
|
-
Pins:: `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`
|
|
258
|
+
Pins:: `spec/moxml/xsd_builtin_lexical_spec.rb`, `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
|
|
262
|
-
checks).
|
|
262
|
+
checks). The built-in lexical answer is facade-owned:
|
|
263
|
+
`Moxml::XSD.builtin_valid?` evaluates the Datatypes Part 2 lexical
|
|
264
|
+
spaces (collapse for the token family, the unbounded integer family
|
|
265
|
+
with bignum bounds, calendar and duration grammars, binary, QName
|
|
266
|
+
with the reserved `xmlns` prefix) from the facade's own table on
|
|
267
|
+
every adapter — an engine table converges to it, never the reverse;
|
|
268
|
+
an unknown name raises `ArgumentError`. `validate` returns
|
|
269
|
+
accumulated `ValidationError` entries —
|
|
263
270
|
message always; line/column/path where the engine reports them, nil
|
|
264
271
|
where it does not; `[]` means valid. Documents outside the engine's
|
|
265
272
|
scope validate laxly, except that strict `xs:any` /
|
|
@@ -286,8 +293,14 @@ registered member must realize a position — any choice/all,
|
|
|
286
293
|
cardinality violation, or unknown name keeps the engine's answer.
|
|
287
294
|
A compiled schema reports the
|
|
288
295
|
version its source declares: root `vc:minVersion="1.1"` scopes the
|
|
289
|
-
document to 1.1, anything else is 1.0
|
|
290
|
-
|
|
296
|
+
document to 1.1, anything else is 1.0 — on engines that serve their
|
|
297
|
+
own effective version the engine's answer can only UPGRADE the
|
|
298
|
+
reported value to 1.1, never downgrade the source annotation
|
|
299
|
+
(consumers scope 1.0-only checks with it; applying 1.1 semantics is
|
|
300
|
+
the engine-side program). Engines that expose a structured report
|
|
301
|
+
surface `Schema#structured_errors` — one row per finding with
|
|
302
|
+
kind/component/line/column/message; the face answers nil on
|
|
303
|
+
bindings without it and #validate keeps the flat shape.
|
|
291
304
|
Unsupported adapters raise
|
|
292
305
|
`Moxml::NotImplementedError` at compile; `Moxml::XSD.supported?`
|
|
293
306
|
answers without raising. RELAX NG rides engines with an RNG face
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -373,11 +373,6 @@ module Moxml
|
|
|
373
373
|
"XSD validation requires an adapter with an XSD engine"
|
|
374
374
|
end
|
|
375
375
|
|
|
376
|
-
def xsd_builtin_valid?(_builtin, _lexical)
|
|
377
|
-
raise Moxml::NotImplementedError,
|
|
378
|
-
"XSD validation requires an adapter with an XSD engine"
|
|
379
|
-
end
|
|
380
|
-
|
|
381
376
|
def xsd_declarations(_xsd_handle); end
|
|
382
377
|
|
|
383
378
|
def xsd_compile_error(_xsd_handle); end
|
|
@@ -406,15 +406,6 @@ module Moxml
|
|
|
406
406
|
handle.valid?(to_binding(document))
|
|
407
407
|
end
|
|
408
408
|
|
|
409
|
-
def xsd_builtin_valid?(builtin, lexical)
|
|
410
|
-
unless XSD_SUPPORTED
|
|
411
|
-
raise Moxml::NotImplementedError,
|
|
412
|
-
"XSD validation requires an adapter with an XSD engine"
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
::Leptris::XML::XSD.builtin_valid?(builtin, lexical)
|
|
416
|
-
end
|
|
417
|
-
|
|
418
409
|
def xsd_declarations(xsd_handle)
|
|
419
410
|
xsd_handle.declaration_count
|
|
420
411
|
end
|
|
@@ -425,6 +416,34 @@ module Moxml
|
|
|
425
416
|
xsd_handle.release
|
|
426
417
|
end
|
|
427
418
|
|
|
419
|
+
# moxml#460 item 2: the ENGINE's effective version (the
|
|
420
|
+
# authoritative source — it gates the 1.1 semantics). Nil
|
|
421
|
+
# when the binding predates the face (fall back to the
|
|
422
|
+
# rules heuristic on the facade side).
|
|
423
|
+
def xsd_version(xsd_handle)
|
|
424
|
+
return nil unless defined?(::Leptris::XML::XSD::Schema) &&
|
|
425
|
+
xsd_handle.is_a?(::Leptris::XML::XSD::Schema) &&
|
|
426
|
+
xsd_handle.respond_to?(:version)
|
|
427
|
+
|
|
428
|
+
xsd_handle.version
|
|
429
|
+
rescue ::Leptris::XML::Error
|
|
430
|
+
nil
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# moxml#460/#1841: the engine's structured report — one
|
|
434
|
+
# {kind:, component:, line:, column:, message:} hash per
|
|
435
|
+
# finding. Nil when the binding predates the face.
|
|
436
|
+
def xsd_structured_errors(xsd_handle, doc)
|
|
437
|
+
return nil unless defined?(::Leptris::XML::XSD::Schema) &&
|
|
438
|
+
xsd_handle.is_a?(::Leptris::XML::XSD::Schema) &&
|
|
439
|
+
(xsd_handle.respond_to?(:structured_errors) ||
|
|
440
|
+
xsd_handle.respond_to?(:structured_errors, true))
|
|
441
|
+
|
|
442
|
+
xsd_handle.structured_errors(doc)
|
|
443
|
+
rescue ::Leptris::XML::Error
|
|
444
|
+
nil
|
|
445
|
+
end
|
|
446
|
+
|
|
428
447
|
def xsd_compile_error(xsd_handle)
|
|
429
448
|
xsd_handle.error
|
|
430
449
|
end
|
|
@@ -911,9 +930,20 @@ module Moxml
|
|
|
911
930
|
# Protocol-level node equality: one engine can hand out
|
|
912
931
|
# two wrapper classes over the same C node (native layer +
|
|
913
932
|
# binding), so raw == is not identity across the seam.
|
|
933
|
+
# Attributes are (name, value, element) triples without a
|
|
934
|
+
# node pointer; identity is the owning element plus the
|
|
935
|
+
# attribute name.
|
|
914
936
|
def same_node?(one, other)
|
|
915
937
|
return true if one.equal?(other)
|
|
916
938
|
|
|
939
|
+
if one.is_a?(::Leptris::XML::Attr) ||
|
|
940
|
+
other.is_a?(::Leptris::XML::Attr)
|
|
941
|
+
return one.is_a?(::Leptris::XML::Attr) &&
|
|
942
|
+
other.is_a?(::Leptris::XML::Attr) &&
|
|
943
|
+
one.name == other.name &&
|
|
944
|
+
same_node?(one.element, other.element)
|
|
945
|
+
end
|
|
946
|
+
|
|
917
947
|
if NATIVE_READ_LAYER
|
|
918
948
|
native_one = one.is_a?(::Leptris::XML::NativeNode)
|
|
919
949
|
native_other = other.is_a?(::Leptris::XML::NativeNode)
|
|
@@ -3135,6 +3165,14 @@ module Moxml
|
|
|
3135
3165
|
node = to_binding(node)
|
|
3136
3166
|
new_node = to_binding(new_node)
|
|
3137
3167
|
end
|
|
3168
|
+
# anchor the BINDING document: its handle owns the engine
|
|
3169
|
+
# pool — while it lives, no retained address can dangle
|
|
3170
|
+
if node.respond_to?(:document) && new_node.respond_to?(:document)
|
|
3171
|
+
target = node.document
|
|
3172
|
+
bsrc = new_node.document
|
|
3173
|
+
target.retain_document!(bsrc) if target.respond_to?(:retain_document!) &&
|
|
3174
|
+
bsrc.respond_to?(:freed?)
|
|
3175
|
+
end
|
|
3138
3176
|
pin_foreign_owner_document(node, new_node)
|
|
3139
3177
|
# A PI inserted before the root lives at document level in
|
|
3140
3178
|
# libleptris's model, not in the element tree.
|
|
@@ -3284,6 +3322,27 @@ module Moxml
|
|
|
3284
3322
|
end
|
|
3285
3323
|
|
|
3286
3324
|
def replace(node, new_node)
|
|
3325
|
+
# leptris#1866: keep a cross-document source (a scratch doc
|
|
3326
|
+
# parsed only to build the replacement) anchored on the
|
|
3327
|
+
# target — wrapper caches on the target side may hold
|
|
3328
|
+
# references the engine copy cannot see. STRICTLY
|
|
3329
|
+
# best-effort: bare adapter-contract nodes carry no
|
|
3330
|
+
# context/document, and retention must never break a
|
|
3331
|
+
# replace.
|
|
3332
|
+
begin
|
|
3333
|
+
target = node.document if node.respond_to?(:document)
|
|
3334
|
+
source = new_node.document if new_node.respond_to?(:document)
|
|
3335
|
+
if target.respond_to?(:retain_document!) &&
|
|
3336
|
+
source.respond_to?(:retain_document!)
|
|
3337
|
+
target.retain_document!(source)
|
|
3338
|
+
# the scratch CONTEXT (config + adapter) is part of the
|
|
3339
|
+
# retained object graph — its death frees the native
|
|
3340
|
+
# registrations even when the document wrapper survives
|
|
3341
|
+
target.retain_document!(new_node.context)
|
|
3342
|
+
end
|
|
3343
|
+
rescue StandardError
|
|
3344
|
+
# context/document unreadable on some wrapper flavors
|
|
3345
|
+
end
|
|
3287
3346
|
if NATIVE_READ_LAYER
|
|
3288
3347
|
node = to_binding(node)
|
|
3289
3348
|
new_node = to_binding(new_node)
|
|
@@ -3297,9 +3356,16 @@ module Moxml
|
|
|
3297
3356
|
# tree cycles (standoc term cleanup does
|
|
3298
3357
|
# t << n.replace(t2)). Insert + unlink instead: the unlink
|
|
3299
3358
|
# path clears the sibling links.
|
|
3300
|
-
|
|
3359
|
+
# leptris#1866: return the INSTALLED sibling, not
|
|
3360
|
+
# new_node — a cross-document new_node belongs to its
|
|
3361
|
+
# scratch document's arena (the insert deep-copies), and
|
|
3362
|
+
# the temp wrapper returned here used to be stored in the
|
|
3363
|
+
# parent's children memo; once the scratch was collected
|
|
3364
|
+
# the memo handed out freed addresses (serializer
|
|
3365
|
+
# segfault + empty attribute names in the output).
|
|
3366
|
+
installed = node.add_next_sibling(new_node)
|
|
3301
3367
|
node.unlink
|
|
3302
|
-
return new_node
|
|
3368
|
+
return installed || new_node
|
|
3303
3369
|
end
|
|
3304
3370
|
|
|
3305
3371
|
# libleptris only offers element-anchored insertion, so a
|
|
@@ -3326,6 +3392,9 @@ module Moxml
|
|
|
3326
3392
|
prev.c_ptr, new_node.c_ptr
|
|
3327
3393
|
),
|
|
3328
3394
|
)
|
|
3395
|
+
# leptris#1866: the raw face deep-copies; the caller must
|
|
3396
|
+
# never keep the scratch wrapper (see the element branch).
|
|
3397
|
+
prev.next_sibling || new_node
|
|
3329
3398
|
else
|
|
3330
3399
|
advance_mutation_version(parent)
|
|
3331
3400
|
::Leptris::XML::FFI.check_status(
|
|
@@ -3333,8 +3402,8 @@ module Moxml
|
|
|
3333
3402
|
parent.c_ptr, new_node.c_ptr
|
|
3334
3403
|
),
|
|
3335
3404
|
)
|
|
3405
|
+
parent.children.last || new_node
|
|
3336
3406
|
end
|
|
3337
|
-
new_node
|
|
3338
3407
|
end
|
|
3339
3408
|
|
|
3340
3409
|
NATIVE_APPEND_CHILDREN =
|
|
@@ -3659,8 +3728,16 @@ module Moxml
|
|
|
3659
3728
|
return nil if namespaces && !namespaces.empty?
|
|
3660
3729
|
# Reverse axes answer reverse-ordered from the native
|
|
3661
3730
|
# engine; route them to the Ruby engine (document order).
|
|
3662
|
-
|
|
3731
|
+
# The parent step is the exception on fixed bindings
|
|
3732
|
+
# (PARENT_AXIS_NATIVE): it answers the owner element and
|
|
3733
|
+
# the hot consumers are element-context paths (#474).
|
|
3734
|
+
if REVERSE_AXIS_CACHE.get_or_set(expression) do
|
|
3663
3735
|
expression_uses_reverse_axis?(expression)
|
|
3736
|
+
end && !(PARENT_AXIS_NATIVE &&
|
|
3737
|
+
!NON_PARENT_REVERSE_CACHE.get_or_set(expression) do
|
|
3738
|
+
expression_uses_non_parent_reverse_axis?(expression)
|
|
3739
|
+
end)
|
|
3740
|
+
return nil
|
|
3664
3741
|
end
|
|
3665
3742
|
|
|
3666
3743
|
# The binding reports the root element parentless while moxml
|
|
@@ -3778,7 +3855,19 @@ module Moxml
|
|
|
3778
3855
|
# expressions route to the Ruby engine, which orders them.
|
|
3779
3856
|
REVERSE_AXES = %w[ancestor ancestor-or-self preceding
|
|
3780
3857
|
preceding-sibling parent].freeze
|
|
3858
|
+
# Reverse axes answer reverse-ordered from the native engine
|
|
3859
|
+
# (document order is the model's contract), so they route to
|
|
3860
|
+
# the Ruby engine — EXCEPT the parent step on bindings
|
|
3861
|
+
# serving the fixed engine: 1.9.385 carries leptris#1869
|
|
3862
|
+
# (parent-of-attribute names the OWNER element) plus the
|
|
3863
|
+
# cross-doc adopt attribute-chain fix, and standoc's svg
|
|
3864
|
+
# id cleanup rides `./@id/..` hot (moxml#474).
|
|
3865
|
+
PARENT_AXIS_NATIVE =
|
|
3866
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.385")
|
|
3867
|
+
NON_PARENT_REVERSE_AXES = %w[ancestor ancestor-or-self
|
|
3868
|
+
preceding preceding-sibling].freeze
|
|
3781
3869
|
REVERSE_AXIS_CACHE = XPath::Cache.new(200)
|
|
3870
|
+
NON_PARENT_REVERSE_CACHE = XPath::Cache.new(200)
|
|
3782
3871
|
|
|
3783
3872
|
def expression_uses_parent_axis?(expression)
|
|
3784
3873
|
ast = XPath::Parser.parse_with_cache(expression)
|
|
@@ -3805,6 +3894,25 @@ module Moxml
|
|
|
3805
3894
|
end
|
|
3806
3895
|
end
|
|
3807
3896
|
|
|
3897
|
+
def expression_uses_non_parent_reverse_axis?(expression)
|
|
3898
|
+
ast = XPath::Parser.parse_with_cache(expression)
|
|
3899
|
+
contains_non_parent_reverse_axis?(ast)
|
|
3900
|
+
rescue XPath::SyntaxError
|
|
3901
|
+
true
|
|
3902
|
+
end
|
|
3903
|
+
|
|
3904
|
+
def contains_non_parent_reverse_axis?(ast)
|
|
3905
|
+
if ast.type == :axis && NON_PARENT_REVERSE_AXES.include?(ast.children.first)
|
|
3906
|
+
return true
|
|
3907
|
+
end
|
|
3908
|
+
return false if ast.type == :parent
|
|
3909
|
+
|
|
3910
|
+
ast.children.any? do |child|
|
|
3911
|
+
child.is_a?(XPath::AST::Node) &&
|
|
3912
|
+
contains_non_parent_reverse_axis?(child)
|
|
3913
|
+
end
|
|
3914
|
+
end
|
|
3915
|
+
|
|
3808
3916
|
def contains_parent_axis?(ast)
|
|
3809
3917
|
return true if ast.type == :parent
|
|
3810
3918
|
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
data/lib/moxml/xpath/ordering.rb
CHANGED
|
@@ -75,7 +75,18 @@ module Moxml
|
|
|
75
75
|
[kind_rank, ancestry_path(node)]
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Attributes are not children of their element, so their
|
|
79
|
+
# document position is the parent's; same-parent attributes
|
|
80
|
+
# keep their relative order (stable sort). A parentless
|
|
81
|
+
# attribute stays unplaceable.
|
|
78
82
|
def ancestry_path(node)
|
|
83
|
+
if node.is_a?(Moxml::Attribute)
|
|
84
|
+
parent = node.parent
|
|
85
|
+
return [2_000_000] unless parent
|
|
86
|
+
|
|
87
|
+
return ancestry_path(parent)
|
|
88
|
+
end
|
|
89
|
+
|
|
79
90
|
path = []
|
|
80
91
|
current = node
|
|
81
92
|
while current && !current.is_a?(Moxml::Document)
|