moxml 0.5.148 → 0.5.149
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/.github/workflows/nightly-fuzz.yml +80 -0
- data/docs/_pages/divergences.adoc +58 -0
- data/docs/_references/processing-model.adoc +30 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/leptris.rb +110 -19
- data/lib/moxml/adapter/libxml.rb +145 -23
- data/lib/moxml/adapter/oga.rb +33 -3
- data/lib/moxml/adapter/ox.rb +26 -2
- data/lib/moxml/adapter/rexml.rb +65 -15
- data/lib/moxml/context.rb +5 -1
- data/lib/moxml/document.rb +6 -6
- data/lib/moxml/node.rb +22 -6
- data/lib/moxml/node_set.rb +11 -0
- data/lib/moxml/reference/normalizer.rb +63 -0
- data/lib/moxml/reference/parser.rb +417 -0
- data/lib/moxml/reference/writer.rb +121 -0
- data/lib/moxml/reference.rb +205 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +17 -2
- data/lib/moxml/xpath/ordering.rb +97 -0
- data/lib/moxml/xpath.rb +1 -0
- data/lib/moxml.rb +1 -0
- data/spec/consistency/adapter_parity_spec.rb +2 -0
- data/spec/consistency/differential_taxonomy_spec.rb +117 -0
- data/spec/consistency/divergence_register_spec.rb +9 -1
- data/spec/consistency/eol_normalization_spec.rb +2 -0
- data/spec/consistency/loud_degradation_spec.rb +2 -0
- data/spec/consistency/metamorphic_properties_spec.rb +195 -0
- data/spec/consistency/metamorphic_spec.rb +138 -0
- data/spec/consistency/model_coverage_audit_spec.rb +108 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +11 -0
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/attribute_resolver_spec.rb +2 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +2 -0
- data/spec/moxml/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/entity_spec.rb +2 -0
- data/spec/moxml/plan_conformance_spec.rb +2 -0
- data/spec/moxml/processing_instruction_spec.rb +9 -0
- data/spec/moxml/reference_opal_spec.rb +29 -0
- data/spec/moxml/reference_spec.rb +162 -0
- data/spec/moxml/sax_spec.rb +2 -0
- data/spec/moxml/text_spec.rb +2 -0
- data/spec/moxml/to_html_spec.rb +2 -0
- data/spec/moxml/xhtml_output_spec.rb +2 -0
- data/spec/moxml/xpath/axes_spec.rb +2 -2
- data/spec/moxml/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +212 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3af023098a8dde5521f0297f240a1c6fa71625cc2b3cd6924eba638724e12517
|
|
4
|
+
data.tar.gz: 9fb194d059475ed0c4aea3471b9048195ce7ea0bef349c25bdc52f991944ca5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 860ac49f2684d504273e22dcbc9c6ed43106ebf67d9ca093af74543ab284e89c7be5c02d3991421973674837ece981127ede74329bfce9a352236bf9b3c094ff
|
|
7
|
+
data.tar.gz: 24b50dee831eb6856dc3f24b7759e32aead60c98896427ed7776277cd69aca0db01f3521841cbc556ccfaaee8fca56a071f2f219276f0553fe919b712c87ef2b
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: nightly fuzz
|
|
2
|
+
|
|
3
|
+
# TODO.sota/02's nightly leg: the property suites swept at random
|
|
4
|
+
# seeds and larger case counts, with findings filed as issues carrying
|
|
5
|
+
# a minimal repro. PR legs run the same suites at a fixed, bounded
|
|
6
|
+
# seed (MOXML_FUZZ/CI defaults in the spec); this leg is the breadth
|
|
7
|
+
# pass, not a gate.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
schedule:
|
|
11
|
+
- cron: "23 3 * * *"
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
issues: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
fuzz:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v6
|
|
23
|
+
with:
|
|
24
|
+
submodules: "recursive"
|
|
25
|
+
|
|
26
|
+
- name: Set up Ruby
|
|
27
|
+
uses: ruby/setup-ruby@v1
|
|
28
|
+
with:
|
|
29
|
+
ruby-version: "3.4"
|
|
30
|
+
bundler-cache: false
|
|
31
|
+
|
|
32
|
+
- name: Install ragel
|
|
33
|
+
run: sudo apt-get update && sudo apt-get install -y ragel
|
|
34
|
+
|
|
35
|
+
- name: Install ruby-ll (for rake vendor:prepare)
|
|
36
|
+
run: gem install ruby-ll --no-document
|
|
37
|
+
|
|
38
|
+
- name: Generate ragel / ruby-ll outputs in vendored forks
|
|
39
|
+
run: rake vendor:prepare
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: bundle install
|
|
43
|
+
|
|
44
|
+
- name: Compile liboga / libll native extensions
|
|
45
|
+
run: rake vendor:compile
|
|
46
|
+
|
|
47
|
+
- name: Pick fuzz seeds
|
|
48
|
+
run: |
|
|
49
|
+
SEEDS=$(ruby -e 'srand; puts Array.new(3) { rand(1_000_000) }.join(",")')
|
|
50
|
+
echo "SOTA_FUZZ_SEEDS=$SEEDS" >> "$GITHUB_ENV"
|
|
51
|
+
echo "SOTA_FUZZ_CASES=1000" >> "$GITHUB_ENV"
|
|
52
|
+
|
|
53
|
+
- name: Fuzz (properties + structural suite)
|
|
54
|
+
run: |
|
|
55
|
+
bundle exec rspec \
|
|
56
|
+
spec/consistency/metamorphic_properties_spec.rb \
|
|
57
|
+
spec/consistency/metamorphic_spec.rb 2>&1 | tee fuzz.log
|
|
58
|
+
|
|
59
|
+
- name: File an issue on findings
|
|
60
|
+
if: failure()
|
|
61
|
+
env:
|
|
62
|
+
GH_TOKEN: ${{ github.token }}
|
|
63
|
+
run: |
|
|
64
|
+
{
|
|
65
|
+
echo "The nightly fuzz leg found findings at seeds \`$SOTA_FUZZ_SEEDS\` (\`SOTA_FUZZ_CASES=$SOTA_FUZZ_CASES\`)."
|
|
66
|
+
echo
|
|
67
|
+
echo "Reproduce:"
|
|
68
|
+
echo
|
|
69
|
+
echo '```'
|
|
70
|
+
echo "SOTA_FUZZ_SEEDS=$SOTA_FUZZ_SEEDS SOTA_FUZZ_CASES=$SOTA_FUZZ_CASES \\"
|
|
71
|
+
echo " bundle exec rspec spec/consistency/metamorphic_properties_spec.rb spec/consistency/metamorphic_spec.rb"
|
|
72
|
+
echo '```'
|
|
73
|
+
echo
|
|
74
|
+
echo '```'
|
|
75
|
+
grep -E "Failure/Error|gen [0-9]+/|rspec \./" fuzz.log | head -40
|
|
76
|
+
echo '```'
|
|
77
|
+
} > fuzz-issue.md
|
|
78
|
+
gh issue create \
|
|
79
|
+
--title "nightly fuzz findings (seeds $SOTA_FUZZ_SEEDS)" \
|
|
80
|
+
--body-file fuzz-issue.md
|
|
@@ -87,4 +87,62 @@ attribute-value predicates, namespace/default-namespace tests,
|
|
|
87
87
|
`position()`/`count()`, `text()`, boolean expressions, `child::`/
|
|
88
88
|
`descendant::` axes, or unions. Pinned in
|
|
89
89
|
`spec/moxml/xpath_capabilities_spec.rb` and `spec/moxml/node_spec.rb`.
|
|
90
|
+
|
|
91
|
+
| D-13
|
|
92
|
+
| ox, headed_ox
|
|
93
|
+
| Inter-element whitespace-only text nodes are dropped at parse
|
|
94
|
+
(node counts and leading/trailing text differ from the other
|
|
95
|
+
adapters). Surfaced by the taxonomy differential. Pinned in
|
|
96
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
97
|
+
|
|
98
|
+
| D-14
|
|
99
|
+
| oga
|
|
100
|
+
| Processing-instruction content leaks into text reads (oga's own
|
|
101
|
+
`text` folds PIs; the model says only text nodes contribute).
|
|
102
|
+
Surfaced by the taxonomy differential. Pinned in
|
|
103
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
104
|
+
|
|
105
|
+
| D-15
|
|
106
|
+
| rexml
|
|
107
|
+
| The whitespace text node between a pre-root PI and the root is
|
|
108
|
+
retained (other adapters drop it). Surfaced by the taxonomy
|
|
109
|
+
differential. Pinned in `spec/consistency/differential_taxonomy_spec.rb`.
|
|
110
|
+
|
|
111
|
+
| D-16
|
|
112
|
+
| rexml
|
|
113
|
+
| Entity-expansion guard caps huge documents (~10k expansions) and
|
|
114
|
+
raised a bare engine error — the facade now names it a ParseError;
|
|
115
|
+
the cap itself remains (nokogiri's HUGE lane has no such cap).
|
|
116
|
+
Surfaced by the taxonomy differential. Pinned in
|
|
117
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
118
|
+
|
|
119
|
+
| D-17
|
|
120
|
+
| oga
|
|
121
|
+
| The strict parse lane does not validate nesting well-formedness
|
|
122
|
+
(oga's strict is lexer-strict, not well-formedness-strict); bad
|
|
123
|
+
nesting parses where other adapters raise. Surfaced by the taxonomy
|
|
124
|
+
differential. Pinned in
|
|
125
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
126
|
+
|
|
127
|
+
| D-18
|
|
128
|
+
| leptris, oga, ox, headed_ox
|
|
129
|
+
| Strict parse accepts invalid UTF-8 byte sequences (no encoding
|
|
130
|
+
validation at parse; the bytes flow through as binary). The
|
|
131
|
+
validating adapters (nokogiri, rexml, libxml) raise the named
|
|
132
|
+
ParseError. On leptris the facade raises the named ParseError in the
|
|
133
|
+
strict lane (default-UTF-8 rule; prolog-declared non-UTF-8 encodings
|
|
134
|
+
own their validity) pending engine-side validation —
|
|
135
|
+
leptris-ruby#514. Surfaced by the taxonomy differential. Pinned in
|
|
136
|
+
`spec/consistency/differential_taxonomy_spec.rb`.
|
|
137
|
+
|
|
138
|
+
| D-19
|
|
139
|
+
| nokogiri, libxml
|
|
140
|
+
| Adjacent text-like leaves (CDATA next to CDATA or text) coalesce
|
|
141
|
+
into a single text node on the libxml2 lane; oga, rexml, ox,
|
|
142
|
+
headed_ox, and leptris keep them as distinct nodes. The `text`
|
|
143
|
+
answer is identical — the node count and kinds differ. The model is
|
|
144
|
+
silent on text-node coalescing (an open decision). Surfaced by the
|
|
145
|
+
generated metamorphic differential (M5). Pinned in
|
|
146
|
+
`spec/consistency/metamorphic_properties_spec.rb` through
|
|
147
|
+
`InputGenerator::FEATURE_EXEMPT`.
|
|
90
148
|
|===
|
|
@@ -18,9 +18,20 @@ skip with their entry ID — an undocumented divergence is a bug.
|
|
|
18
18
|
=== Status and precedence
|
|
19
19
|
|
|
20
20
|
1. This document — the model.
|
|
21
|
-
2. `spec/consistency/` — the model pinned as executable expectations
|
|
21
|
+
2. `spec/consistency/` — the model pinned as executable expectations,
|
|
22
|
+
with `lib/moxml/reference/` as the **engine-free executable
|
|
23
|
+
reference** (a pure-Ruby reader, writer, and normalizer). Adapters
|
|
24
|
+
are differentially tested against the reference
|
|
25
|
+
(`spec/consistency/reference_differential_spec.rb`), not only
|
|
26
|
+
against each other; the reference is pure Ruby and transpiles to
|
|
27
|
+
the Opal lane.
|
|
22
28
|
3. Adapter implementations and capability gates — representations.
|
|
23
29
|
|
|
30
|
+
A pin cites the model section it enforces
|
|
31
|
+
(`# Processing model §<section> (normative)`); the coverage audit
|
|
32
|
+
asserts both directions of that linkage, so removing any of the
|
|
33
|
+
document, a pin, or the reference fails CI.
|
|
34
|
+
|
|
24
35
|
A semantics change amends this document first (see
|
|
25
36
|
`TODO.model/05-model-evolution-protocol.md`).
|
|
26
37
|
|
|
@@ -48,6 +59,12 @@ the same underlying node; node identity is a property of the
|
|
|
48
59
|
underlying tree, not of wrapper object identity. Comparisons use node
|
|
49
60
|
equality semantics, never `equal?` on wrappers.
|
|
50
61
|
|
|
62
|
+
Kind:: Every node answers `node_type` — one of `:document`,
|
|
63
|
+
`:element`, `:attribute`, `:text`, `:comment`, `:cdata`,
|
|
64
|
+
`:processing_instruction`, `:doctype`, `:declaration`,
|
|
65
|
+
`:entity_reference`, `:namespace` — the predicates (`element?`,
|
|
66
|
+
`text?`, …) are its derivatives.
|
|
67
|
+
|
|
51
68
|
Ownership:: The `Document` owns the tree's lifetime; nodes are valid
|
|
52
69
|
within it. Deterministic release is available via `Document#free`.
|
|
53
70
|
|
|
@@ -61,6 +78,10 @@ listings or serialization child walks.
|
|
|
61
78
|
* Walks (`each_node`, `materialize`) visit depth-first, pre-order.
|
|
62
79
|
* `Document#children` and `#root` never include the declaration;
|
|
63
80
|
doctype exposure follows the Nokogiri contract.
|
|
81
|
+
* Document-level parts (`Declaration`, `Doctype`) are state, not
|
|
82
|
+
nodes: walks (`traverse`, `each_node`, `materialize`) never yield
|
|
83
|
+
them, and walks over a DOCTYPE-bearing document are well-defined on
|
|
84
|
+
every adapter.
|
|
64
85
|
* `NodeSet` preserves document order for axis results; unions preserve
|
|
65
86
|
first-encounter document order.
|
|
66
87
|
|
|
@@ -86,7 +107,9 @@ content; entity references restored when the document is
|
|
|
86
107
|
entity-bearing.
|
|
87
108
|
|
|
88
109
|
`content`:: The node's content per its kind; on text-like nodes, with
|
|
89
|
-
entity-marker restoration; memoized against mutation.
|
|
110
|
+
entity-marker restoration; memoized against mutation. A processing
|
|
111
|
+
instruction's content is its data **without** the separating
|
|
112
|
+
whitespace after the target (uniform across adapters).
|
|
90
113
|
|
|
91
114
|
`raw_content`:: The backing text **without** entity restoration.
|
|
92
115
|
|
|
@@ -128,6 +151,9 @@ with an unambiguous local name.
|
|
|
128
151
|
`xmlns:` default-namespace matching; attribute terminals in every
|
|
129
152
|
position (`//a/@id`, `//@id`, `r/a/@id`); symbol- and string-keyed
|
|
130
153
|
bindings resolve identically.
|
|
154
|
+
* `at_xpath` is sugar over `xpath`: it answers the first node of a
|
|
155
|
+
node-set and passes scalar results (`count()`, `string()`,
|
|
156
|
+
`boolean()`) through unwrapped, uniformly across adapters.
|
|
131
157
|
|
|
132
158
|
=== Serialization
|
|
133
159
|
|
|
@@ -137,7 +163,8 @@ with an unambiguous local name.
|
|
|
137
163
|
|
|
138
164
|
| `to_xml`
|
|
139
165
|
| Declaration per configuration; empty elements per `expand_empty`;
|
|
140
|
-
namespace declarations preserved
|
|
166
|
+
namespace declarations preserved; a parsed DOCTYPE is emitted with
|
|
167
|
+
its external identifiers and internal subset.
|
|
141
168
|
|
|
142
169
|
| `to_html`
|
|
143
170
|
| HTML method: void elements unclosed (`<br>`), raw text
|
|
@@ -62,3 +62,7 @@ require "moxml/xpath/errors"
|
|
|
62
62
|
require "moxml/xpath/ast/node"
|
|
63
63
|
require "moxml/xpath/ruby/node"
|
|
64
64
|
require "moxml/xpath/ruby/generator"
|
|
65
|
+
require "moxml/reference"
|
|
66
|
+
require "moxml/reference/parser"
|
|
67
|
+
require "moxml/reference/writer"
|
|
68
|
+
require "moxml/reference/normalizer"
|
|
@@ -348,9 +348,13 @@ module Moxml
|
|
|
348
348
|
|
|
349
349
|
def xsd_content_valid?(xsd_handle, element_name, children)
|
|
350
350
|
# The binding reads each child's name + effective
|
|
351
|
-
# namespace through its own node API —
|
|
351
|
+
# namespace through its own node API (c_ptr) — bridge to
|
|
352
|
+
# the binding's nodes. 1.9.374+ mints registered klasses
|
|
353
|
+
# at create, so children arrive as Identity wrappers whose
|
|
354
|
+
# .native is self (no c_ptr); to_binding hands back the
|
|
355
|
+
# binding node whatever the wrapper flavor.
|
|
352
356
|
xsd_handle.content_valid?(
|
|
353
|
-
element_name, children.map(
|
|
357
|
+
element_name, children.map { |c| to_binding(c.native) }
|
|
354
358
|
)
|
|
355
359
|
end
|
|
356
360
|
|
|
@@ -525,12 +529,18 @@ module Moxml
|
|
|
525
529
|
include ::Moxml::Node
|
|
526
530
|
include ::Moxml::ProcessingInstruction
|
|
527
531
|
|
|
528
|
-
# Same nil bridge as Comment (leptris-ruby#344)
|
|
532
|
+
# Same nil bridge as Comment (leptris-ruby#344); the
|
|
533
|
+
# separating whitespace after the target is not content
|
|
534
|
+
# (uniform PI semantics, TODO.sota/02).
|
|
529
535
|
def content
|
|
530
536
|
content = native_content
|
|
531
|
-
|
|
537
|
+
if content.nil?
|
|
538
|
+
return ::Moxml::Adapter::Leptris.to_binding(self)
|
|
539
|
+
.content.to_s
|
|
540
|
+
.sub(/\A\s+/, "")
|
|
541
|
+
end
|
|
532
542
|
|
|
533
|
-
content
|
|
543
|
+
content.sub(/\A\s+/, "")
|
|
534
544
|
end
|
|
535
545
|
end
|
|
536
546
|
|
|
@@ -1174,8 +1184,36 @@ module Moxml
|
|
|
1174
1184
|
true
|
|
1175
1185
|
end
|
|
1176
1186
|
|
|
1187
|
+
# UTF-8 is the default effective encoding; a prolog declaring
|
|
1188
|
+
# another encoding hands byte validity to that encoding's lane.
|
|
1189
|
+
DECLARED_ENCODING_RE =
|
|
1190
|
+
/<\?xml\s[^>]*?encoding\s*=\s*["']([^"']+)["']/i
|
|
1191
|
+
private_constant :DECLARED_ENCODING_RE
|
|
1192
|
+
|
|
1193
|
+
def encoding_lane_owns_validity?(xml_string)
|
|
1194
|
+
# Binary-tagged head: the prolog is ASCII, and matching
|
|
1195
|
+
# never raises on binary-tagged bytes (invalid-UTF-8 input
|
|
1196
|
+
# is exactly the case this check exists for).
|
|
1197
|
+
declared = xml_string.b[0, 256].match(DECLARED_ENCODING_RE)
|
|
1198
|
+
declared &&
|
|
1199
|
+
!declared[1].match?(/\A(?:utf-?8|us-ascii|ascii)\z/i)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1177
1202
|
def parse(xml, options = {}, _context = nil)
|
|
1178
1203
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
1204
|
+
# Strict parse raises on bytes invalid in the effective
|
|
1205
|
+
# encoding (default UTF-8; declared non-UTF-8 encodings own
|
|
1206
|
+
# their validity) — the validating engines do this at load
|
|
1207
|
+
# (libxml2: "Invalid bytes in character encoding"), the
|
|
1208
|
+
# leptris engine lane does not yet (leptris-ruby#514), so
|
|
1209
|
+
# the facade names it (taxonomy differential, D-18).
|
|
1210
|
+
if options[:strict] && !xml_string.valid_encoding? &&
|
|
1211
|
+
!encoding_lane_owns_validity?(xml_string)
|
|
1212
|
+
raise Moxml::ParseError.new(
|
|
1213
|
+
"Invalid bytes in character encoding (#{xml_string.encoding})",
|
|
1214
|
+
source: xml_string[0, 100],
|
|
1215
|
+
)
|
|
1216
|
+
end
|
|
1179
1217
|
# The marker flag rides preprocess's own `&` scan — no
|
|
1180
1218
|
# second full-buffer probe (issue #132 parse-side note).
|
|
1181
1219
|
entity_mode_keep =
|
|
@@ -1213,7 +1251,11 @@ module Moxml
|
|
|
1213
1251
|
options: parse_flags(options, recover: recover,
|
|
1214
1252
|
context: _context),
|
|
1215
1253
|
)
|
|
1216
|
-
rescue ::Leptris::XML::
|
|
1254
|
+
rescue ::Leptris::XML::Error => e
|
|
1255
|
+
# Invalid encoding bytes surface as the base engine
|
|
1256
|
+
# error class, not ParseError — parse failures are all
|
|
1257
|
+
# named by the facade (taxonomy differential, malformed
|
|
1258
|
+
# class).
|
|
1217
1259
|
raise Moxml::ParseError.new(e.message) if options[:strict]
|
|
1218
1260
|
|
|
1219
1261
|
# Engines below the 1.9.344 recover floor (or the
|
|
@@ -1383,7 +1425,11 @@ module Moxml
|
|
|
1383
1425
|
else
|
|
1384
1426
|
::Leptris::XML.parse_html(html_string)
|
|
1385
1427
|
end
|
|
1386
|
-
rescue ::Leptris::XML::
|
|
1428
|
+
rescue ::Leptris::XML::Error => e
|
|
1429
|
+
# ParseError is the common class; other engine errors
|
|
1430
|
+
# (invalid encoding bytes surface as the base class) are
|
|
1431
|
+
# parse failures too — the facade names them all
|
|
1432
|
+
# (taxonomy differential, malformed class).
|
|
1387
1433
|
raise Moxml::ParseError.new(e.message)
|
|
1388
1434
|
end
|
|
1389
1435
|
attachments.set(native_doc, :entity_markers, false)
|
|
@@ -2265,9 +2311,19 @@ module Moxml
|
|
|
2265
2311
|
def attribute_pairs(element)
|
|
2266
2312
|
binding_element = to_binding(element)
|
|
2267
2313
|
return super unless binding_element.respond_to?(:attribute_pairs)
|
|
2268
|
-
return binding_element.attributes_snapshot.to_a if ELEMENT_ATTR_SNAPSHOT
|
|
2269
2314
|
|
|
2270
|
-
|
|
2315
|
+
pairs = if ELEMENT_ATTR_SNAPSHOT
|
|
2316
|
+
binding_element.attributes_snapshot.to_a
|
|
2317
|
+
else
|
|
2318
|
+
binding_element.attribute_pairs
|
|
2319
|
+
end
|
|
2320
|
+
# The engine exposes qualified names; the facade's listing
|
|
2321
|
+
# face is bare (Attribute#name), uniform with every adapter.
|
|
2322
|
+
pairs.map { |(name, value)| [local_name(name), value] }
|
|
2323
|
+
end
|
|
2324
|
+
|
|
2325
|
+
def local_name(name)
|
|
2326
|
+
name.include?(":") ? name.split(":", 2)[1] : name
|
|
2271
2327
|
end
|
|
2272
2328
|
|
|
2273
2329
|
def attribute_element(attr)
|
|
@@ -2959,16 +3015,20 @@ module Moxml
|
|
|
2959
3015
|
end
|
|
2960
3016
|
|
|
2961
3017
|
def processing_instruction_content(node)
|
|
3018
|
+
# The model's PI content excludes the separating whitespace
|
|
3019
|
+
# after the target (Nokogiri semantics; uniform across
|
|
3020
|
+
# adapters — TODO.sota/02).
|
|
2962
3021
|
if NATIVE_READ_LAYER && node.is_a?(NN)
|
|
2963
3022
|
content = NN_CONTENT.bind_call(node)
|
|
2964
3023
|
# NativeNode#content answers nil for PI-kind nodes too
|
|
2965
3024
|
# (leptris-ruby#344 covers comments); bridge for the data.
|
|
2966
|
-
return to_binding(node).content if
|
|
3025
|
+
return to_binding(node).content.to_s.sub(/\A\s+/, "") if
|
|
3026
|
+
content.nil?
|
|
2967
3027
|
|
|
2968
|
-
return content
|
|
3028
|
+
return content.sub(/\A\s+/, "")
|
|
2969
3029
|
end
|
|
2970
3030
|
|
|
2971
|
-
node.content
|
|
3031
|
+
node.content.to_s.sub(/\A\s+/, "")
|
|
2972
3032
|
end
|
|
2973
3033
|
|
|
2974
3034
|
def set_processing_instruction_content(node, content)
|
|
@@ -3104,6 +3164,12 @@ module Moxml
|
|
|
3104
3164
|
# namespace-bound queries to the Ruby engine until the
|
|
3105
3165
|
# engine grows correct prefix resolution.
|
|
3106
3166
|
return nil if namespaces && !namespaces.empty?
|
|
3167
|
+
# Reverse axes answer reverse-ordered from the native
|
|
3168
|
+
# engine; route them to the Ruby engine (document order).
|
|
3169
|
+
return nil if REVERSE_AXIS_CACHE.get_or_set(expression) do
|
|
3170
|
+
expression_uses_reverse_axis?(expression)
|
|
3171
|
+
end
|
|
3172
|
+
|
|
3107
3173
|
# The binding reports the root element parentless while moxml
|
|
3108
3174
|
# roots it at the document, so parent-axis queries from the
|
|
3109
3175
|
# root element keep the Ruby engine.
|
|
@@ -3190,6 +3256,14 @@ module Moxml
|
|
|
3190
3256
|
node.is_a?(::Leptris::XML::Element)
|
|
3191
3257
|
end
|
|
3192
3258
|
|
|
3259
|
+
# Reverse axes answer in reverse document order from the
|
|
3260
|
+
# native engine; XPath path results are document-ordered (the
|
|
3261
|
+
# model's order contract — TODO.sota/02, M3), so these
|
|
3262
|
+
# expressions route to the Ruby engine, which orders them.
|
|
3263
|
+
REVERSE_AXES = %w[ancestor ancestor-or-self preceding
|
|
3264
|
+
preceding-sibling parent].freeze
|
|
3265
|
+
REVERSE_AXIS_CACHE = XPath::Cache.new(200)
|
|
3266
|
+
|
|
3193
3267
|
def expression_uses_parent_axis?(expression)
|
|
3194
3268
|
ast = XPath::Parser.parse_with_cache(expression)
|
|
3195
3269
|
contains_parent_axis?(ast)
|
|
@@ -3197,6 +3271,24 @@ module Moxml
|
|
|
3197
3271
|
true
|
|
3198
3272
|
end
|
|
3199
3273
|
|
|
3274
|
+
def expression_uses_reverse_axis?(expression)
|
|
3275
|
+
ast = XPath::Parser.parse_with_cache(expression)
|
|
3276
|
+
contains_reverse_axis?(ast)
|
|
3277
|
+
rescue XPath::SyntaxError
|
|
3278
|
+
false
|
|
3279
|
+
end
|
|
3280
|
+
|
|
3281
|
+
def contains_reverse_axis?(ast)
|
|
3282
|
+
return true if ast.type == :parent
|
|
3283
|
+
if ast.type == :axis && REVERSE_AXES.include?(ast.children.first)
|
|
3284
|
+
return true
|
|
3285
|
+
end
|
|
3286
|
+
|
|
3287
|
+
ast.children.any? do |child|
|
|
3288
|
+
child.is_a?(XPath::AST::Node) && contains_reverse_axis?(child)
|
|
3289
|
+
end
|
|
3290
|
+
end
|
|
3291
|
+
|
|
3200
3292
|
def contains_parent_axis?(ast)
|
|
3201
3293
|
return true if ast.type == :parent
|
|
3202
3294
|
return true if ast.type == :axis && ast.children.first == "parent"
|
|
@@ -3366,13 +3458,12 @@ module Moxml
|
|
|
3366
3458
|
|
|
3367
3459
|
def record_source_declaration(native_doc, xml_string)
|
|
3368
3460
|
head = xml_string[0, 256]
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
end
|
|
3461
|
+
# invalid bytes never reach regexes: named parse errors own
|
|
3462
|
+
# malformed input (taxonomy differential, malformed class)
|
|
3463
|
+
had = xml_string.valid_encoding? &&
|
|
3464
|
+
(head.match?(SOURCE_DECLARATION_RE) ||
|
|
3465
|
+
(head.match?(/\A\s*\z/) &&
|
|
3466
|
+
xml_string.match?(SOURCE_DECLARATION_RE)))
|
|
3376
3467
|
attachments.set(native_doc, :had_source_declaration, had)
|
|
3377
3468
|
end
|
|
3378
3469
|
end
|