moxml 0.5.147 → 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 +180 -20
- 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 +24 -10
- data/lib/moxml/node.rb +34 -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/leptris_contract_reads_spec.rb +29 -0
- data/spec/moxml/adapter/leptris_detached_text_spec.rb +10 -3
- data/spec/moxml/adapter/leptris_parse_lint_spec.rb +12 -0
- 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 +17 -2
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"
|
|
@@ -105,6 +105,9 @@ module Moxml
|
|
|
105
105
|
::Leptris::XML::Document.method(:parse)
|
|
106
106
|
.parameters.any? { |_, key| key == :strict }
|
|
107
107
|
|
|
108
|
+
ADD_CHILD_NATIVE =
|
|
109
|
+
NATIVE_READ_LAYER && NN.method_defined?(:add_child_native)
|
|
110
|
+
|
|
108
111
|
if NATIVE_READ_LAYER
|
|
109
112
|
NN_ATTRIBUTE = NN.instance_method(:attribute)
|
|
110
113
|
NN_NAME = NN.instance_method(:name)
|
|
@@ -345,9 +348,13 @@ module Moxml
|
|
|
345
348
|
|
|
346
349
|
def xsd_content_valid?(xsd_handle, element_name, children)
|
|
347
350
|
# The binding reads each child's name + effective
|
|
348
|
-
# 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.
|
|
349
356
|
xsd_handle.content_valid?(
|
|
350
|
-
element_name, children.map(
|
|
357
|
+
element_name, children.map { |c| to_binding(c.native) }
|
|
351
358
|
)
|
|
352
359
|
end
|
|
353
360
|
|
|
@@ -522,12 +529,18 @@ module Moxml
|
|
|
522
529
|
include ::Moxml::Node
|
|
523
530
|
include ::Moxml::ProcessingInstruction
|
|
524
531
|
|
|
525
|
-
# 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).
|
|
526
535
|
def content
|
|
527
536
|
content = native_content
|
|
528
|
-
|
|
537
|
+
if content.nil?
|
|
538
|
+
return ::Moxml::Adapter::Leptris.to_binding(self)
|
|
539
|
+
.content.to_s
|
|
540
|
+
.sub(/\A\s+/, "")
|
|
541
|
+
end
|
|
529
542
|
|
|
530
|
-
content
|
|
543
|
+
content.sub(/\A\s+/, "")
|
|
531
544
|
end
|
|
532
545
|
end
|
|
533
546
|
|
|
@@ -624,6 +637,31 @@ module Moxml
|
|
|
624
637
|
end
|
|
625
638
|
TEXT.include(TextContractReads)
|
|
626
639
|
end
|
|
640
|
+
|
|
641
|
+
# leptris-ruby#507: children_read joins the alias set. The
|
|
642
|
+
# generation-checked memo STAYS — re-walks of the same
|
|
643
|
+
# document are the common harness/consumer shape and the
|
|
644
|
+
# memo serves them allocation-free (dropping it regressed
|
|
645
|
+
# the walk 2x in GC). Only the MISS gets cheaper: the
|
|
646
|
+
# klass-children face plain-dispatches and returns the
|
|
647
|
+
# already-minted wrappers, so the adapter crossing
|
|
648
|
+
# (bind_call + to_a + keep-refs probe) drops out, and
|
|
649
|
+
# entity-marker documents take super for the split.
|
|
650
|
+
if NN.method_defined?(:children_read)
|
|
651
|
+
module ChildReads
|
|
652
|
+
def children
|
|
653
|
+
@children = nil if @children_gen != context.children_generation
|
|
654
|
+
@children_gen = context.children_generation
|
|
655
|
+
@children ||= if doc_entity_bearing?
|
|
656
|
+
super
|
|
657
|
+
else
|
|
658
|
+
NodeSet.new(children_read, context, self)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
ELEMENT.include(ChildReads)
|
|
663
|
+
TEXT.include(ChildReads)
|
|
664
|
+
end
|
|
627
665
|
end
|
|
628
666
|
|
|
629
667
|
# leptris-ruby#246 klass-propagating reads: the read
|
|
@@ -1146,8 +1184,36 @@ module Moxml
|
|
|
1146
1184
|
true
|
|
1147
1185
|
end
|
|
1148
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
|
+
|
|
1149
1202
|
def parse(xml, options = {}, _context = nil)
|
|
1150
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
|
|
1151
1217
|
# The marker flag rides preprocess's own `&` scan — no
|
|
1152
1218
|
# second full-buffer probe (issue #132 parse-side note).
|
|
1153
1219
|
entity_mode_keep =
|
|
@@ -1185,7 +1251,11 @@ module Moxml
|
|
|
1185
1251
|
options: parse_flags(options, recover: recover,
|
|
1186
1252
|
context: _context),
|
|
1187
1253
|
)
|
|
1188
|
-
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).
|
|
1189
1259
|
raise Moxml::ParseError.new(e.message) if options[:strict]
|
|
1190
1260
|
|
|
1191
1261
|
# Engines below the 1.9.344 recover floor (or the
|
|
@@ -1355,7 +1425,11 @@ module Moxml
|
|
|
1355
1425
|
else
|
|
1356
1426
|
::Leptris::XML.parse_html(html_string)
|
|
1357
1427
|
end
|
|
1358
|
-
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).
|
|
1359
1433
|
raise Moxml::ParseError.new(e.message)
|
|
1360
1434
|
end
|
|
1361
1435
|
attachments.set(native_doc, :entity_markers, false)
|
|
@@ -1464,6 +1538,14 @@ module Moxml
|
|
|
1464
1538
|
count = ffi.leptris_document_parse_diag_count(doc)
|
|
1465
1539
|
return [] if count.zero?
|
|
1466
1540
|
|
|
1541
|
+
# leptris#1771: one face carries the diag's machine-readable
|
|
1542
|
+
# code plus line/column with the entry (nil when the engine
|
|
1543
|
+
# has no token code — pre-#1771 records). The face probe
|
|
1544
|
+
# keeps older bindings on kind+message (issue #408).
|
|
1545
|
+
lint_code = if ffi.respond_to?(:leptris_document_parse_diag_code)
|
|
1546
|
+
ffi.method(:leptris_document_parse_diag_code)
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1467
1549
|
kind_ptr = ::FFI::MemoryPointer.new(:int)
|
|
1468
1550
|
diagnostics = []
|
|
1469
1551
|
count.times do |i|
|
|
@@ -1473,7 +1555,19 @@ module Moxml
|
|
|
1473
1555
|
) == 1
|
|
1474
1556
|
|
|
1475
1557
|
kind = DIAG_KINDS[kind_ptr.read_int] || :unknown
|
|
1476
|
-
|
|
1558
|
+
diag = { kind: kind, message: buffer.read_string }
|
|
1559
|
+
if lint_code
|
|
1560
|
+
code_buf = ::FFI::MemoryPointer.new(:char, 128)
|
|
1561
|
+
line_ptr = ::FFI::MemoryPointer.new(:int)
|
|
1562
|
+
column_ptr = ::FFI::MemoryPointer.new(:int)
|
|
1563
|
+
lint_code.call(doc, i, code_buf, 128, line_ptr, column_ptr)
|
|
1564
|
+
code = code_buf.read_string
|
|
1565
|
+
diag[:code] = code.empty? ? nil : code
|
|
1566
|
+
diag[:line] = line_ptr.read_int.zero? ? nil : line_ptr.read_int
|
|
1567
|
+
diag[:column] =
|
|
1568
|
+
column_ptr.read_int.zero? ? nil : column_ptr.read_int
|
|
1569
|
+
end
|
|
1570
|
+
diagnostics << diag
|
|
1477
1571
|
end
|
|
1478
1572
|
diagnostics
|
|
1479
1573
|
end
|
|
@@ -2217,9 +2311,19 @@ module Moxml
|
|
|
2217
2311
|
def attribute_pairs(element)
|
|
2218
2312
|
binding_element = to_binding(element)
|
|
2219
2313
|
return super unless binding_element.respond_to?(:attribute_pairs)
|
|
2220
|
-
return binding_element.attributes_snapshot.to_a if ELEMENT_ATTR_SNAPSHOT
|
|
2221
2314
|
|
|
2222
|
-
|
|
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
|
|
2223
2327
|
end
|
|
2224
2328
|
|
|
2225
2329
|
def attribute_element(attr)
|
|
@@ -2496,6 +2600,27 @@ module Moxml
|
|
|
2496
2600
|
end
|
|
2497
2601
|
|
|
2498
2602
|
def add_child(parent, child)
|
|
2603
|
+
# Same-document Identity append — the builder hot path. One
|
|
2604
|
+
# document pair decides the verdict: the pin, the absorb
|
|
2605
|
+
# checks and the binding bridges only exist for
|
|
2606
|
+
# cross-document work, and the plain C append (version bump
|
|
2607
|
+
# inside) is the whole job. bind_call, not dispatch — the
|
|
2608
|
+
# contract modules shadow add_child on Identity receivers
|
|
2609
|
+
# (moxml#2560 note below).
|
|
2610
|
+
if NATIVE_MUTATIONS_COHERENT && parent.is_a?(NN) && child.is_a?(NN) &&
|
|
2611
|
+
!child.is_a?(CustomizedLeptris::EntityReference)
|
|
2612
|
+
parent_doc = NN_DOCUMENT.bind_call(parent)
|
|
2613
|
+
if parent_doc
|
|
2614
|
+
child_doc = NN_DOCUMENT.bind_call(child)
|
|
2615
|
+
if child_doc && parent_doc.equal?(child_doc)
|
|
2616
|
+
# add_child_native (leptris-ruby#509) plain-dispatches
|
|
2617
|
+
# on Identity receivers — one frame, no bind_call.
|
|
2618
|
+
return parent.add_child_native(child) if ADD_CHILD_NATIVE
|
|
2619
|
+
|
|
2620
|
+
return NN_ADD_CHILD.bind_call(parent, child)
|
|
2621
|
+
end
|
|
2622
|
+
end
|
|
2623
|
+
end
|
|
2499
2624
|
# 1.9.311: binding-family children (created PIs, texts...) —
|
|
2500
2625
|
# a DIRECT absorb makes the binding add below move the node
|
|
2501
2626
|
# by reference. A source already owned elsewhere would need
|
|
@@ -2890,16 +3015,20 @@ module Moxml
|
|
|
2890
3015
|
end
|
|
2891
3016
|
|
|
2892
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).
|
|
2893
3021
|
if NATIVE_READ_LAYER && node.is_a?(NN)
|
|
2894
3022
|
content = NN_CONTENT.bind_call(node)
|
|
2895
3023
|
# NativeNode#content answers nil for PI-kind nodes too
|
|
2896
3024
|
# (leptris-ruby#344 covers comments); bridge for the data.
|
|
2897
|
-
return to_binding(node).content if
|
|
3025
|
+
return to_binding(node).content.to_s.sub(/\A\s+/, "") if
|
|
3026
|
+
content.nil?
|
|
2898
3027
|
|
|
2899
|
-
return content
|
|
3028
|
+
return content.sub(/\A\s+/, "")
|
|
2900
3029
|
end
|
|
2901
3030
|
|
|
2902
|
-
node.content
|
|
3031
|
+
node.content.to_s.sub(/\A\s+/, "")
|
|
2903
3032
|
end
|
|
2904
3033
|
|
|
2905
3034
|
def set_processing_instruction_content(node, content)
|
|
@@ -3035,6 +3164,12 @@ module Moxml
|
|
|
3035
3164
|
# namespace-bound queries to the Ruby engine until the
|
|
3036
3165
|
# engine grows correct prefix resolution.
|
|
3037
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
|
+
|
|
3038
3173
|
# The binding reports the root element parentless while moxml
|
|
3039
3174
|
# roots it at the document, so parent-axis queries from the
|
|
3040
3175
|
# root element keep the Ruby engine.
|
|
@@ -3121,6 +3256,14 @@ module Moxml
|
|
|
3121
3256
|
node.is_a?(::Leptris::XML::Element)
|
|
3122
3257
|
end
|
|
3123
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
|
+
|
|
3124
3267
|
def expression_uses_parent_axis?(expression)
|
|
3125
3268
|
ast = XPath::Parser.parse_with_cache(expression)
|
|
3126
3269
|
contains_parent_axis?(ast)
|
|
@@ -3128,6 +3271,24 @@ module Moxml
|
|
|
3128
3271
|
true
|
|
3129
3272
|
end
|
|
3130
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
|
+
|
|
3131
3292
|
def contains_parent_axis?(ast)
|
|
3132
3293
|
return true if ast.type == :parent
|
|
3133
3294
|
return true if ast.type == :axis && ast.children.first == "parent"
|
|
@@ -3297,13 +3458,12 @@ module Moxml
|
|
|
3297
3458
|
|
|
3298
3459
|
def record_source_declaration(native_doc, xml_string)
|
|
3299
3460
|
head = xml_string[0, 256]
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
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)))
|
|
3307
3467
|
attachments.set(native_doc, :had_source_declaration, had)
|
|
3308
3468
|
end
|
|
3309
3469
|
end
|