moxml 0.5.148 → 0.5.150
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/dependent-repos.json +7 -2
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.github/workflows/nightly-fuzz.yml +80 -0
- data/.github/workflows/performance-release.yml +79 -0
- data/.github/workflows/performance.yml +59 -0
- data/Rakefile +23 -0
- data/benchmark/README.md +106 -0
- data/benchmark/scoreboard.json +115 -0
- data/benchmark/scoreboard.rb +556 -0
- data/bin/generate-model-coverage +21 -0
- data/docs/_pages/adapter-compatibility.adoc +3 -3
- data/docs/_pages/divergences.adoc +88 -39
- data/docs/_pages/ecosystem.adoc +62 -0
- data/docs/_pages/headed-ox-limitations.adoc +1 -1
- data/docs/_pages/headed-ox.adoc +2 -2
- data/docs/_pages/index.adoc +4 -0
- data/docs/_pages/model-coverage.adoc +57 -0
- data/docs/_pages/performance.adoc +29 -0
- data/docs/_pages/why-moxml.adoc +87 -0
- data/docs/_references/processing-model.adoc +69 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +293 -23
- data/lib/moxml/adapter/libxml.rb +179 -25
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +71 -9
- data/lib/moxml/adapter/ox.rb +34 -2
- data/lib/moxml/adapter/rexml.rb +82 -17
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +33 -1
- data/lib/moxml/document.rb +18 -4
- data/lib/moxml/node.rb +75 -8
- data/lib/moxml/node_set.rb +42 -1
- 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/relaxng.rb +62 -0
- data/lib/moxml/sax/block_handler.rb +7 -0
- data/lib/moxml/sax/handler.rb +20 -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 +29 -0
- data/lib/moxml.rb +2 -0
- data/spec/consistency/adapter_parity_spec.rb +24 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -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 +144 -61
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +34 -5
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +35 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -6
- data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
- data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
- data/spec/moxml/c14n/subset_spec.rb +2 -12
- data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
- data/spec/moxml/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/entity_spec.rb +2 -0
- data/spec/moxml/node_spec.rb +0 -3
- 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/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +47 -74
- 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/xpath_capabilities_spec.rb +11 -38
- data/spec/moxml/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +219 -0
- data/spec/support/model_coverage.rb +159 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +32 -2
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -7,23 +7,26 @@ require "libxml-ruby"
|
|
|
7
7
|
module Moxml
|
|
8
8
|
module Adapter
|
|
9
9
|
class Libxml < Base
|
|
10
|
-
# DOCTYPE prolog
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
# DOCTYPE prolog scanning is manual (index/char scan): a regex
|
|
11
|
+
# over hostile prolog text backtracks polynomially on inputs
|
|
12
|
+
# with repeated <!DOCTYPE prefixes or unterminated subsets
|
|
13
|
+
# (CodeQL rb/polynomial-redos). See doctype_parts.
|
|
14
|
+
DOCTYPE_OPEN_SIZE = 9
|
|
15
|
+
QUOTES = ['"', "'"].freeze
|
|
14
16
|
|
|
15
17
|
autoload :EntityRefRegistry, "moxml/adapter/libxml/entity_ref_registry"
|
|
16
18
|
|
|
17
19
|
# Wrapper class to store DOCTYPE information
|
|
18
20
|
class DoctypeWrapper
|
|
19
21
|
attr_reader :native_doc
|
|
20
|
-
attr_accessor :name, :external_id, :system_id
|
|
22
|
+
attr_accessor :name, :external_id, :system_id, :internal_subset
|
|
21
23
|
|
|
22
|
-
def initialize(doc, name, external_id, system_id)
|
|
24
|
+
def initialize(doc, name, external_id, system_id, internal_subset = nil)
|
|
23
25
|
@native_doc = doc
|
|
24
26
|
@name = name
|
|
25
27
|
@external_id = external_id
|
|
26
28
|
@system_id = system_id
|
|
29
|
+
@internal_subset = internal_subset
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
# Provide native method to match adapter pattern
|
|
@@ -32,7 +35,8 @@ module Moxml
|
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def to_xml
|
|
35
|
-
XmlEmitter.doctype_xml(@name, @external_id, @system_id
|
|
38
|
+
XmlEmitter.doctype_xml(@name, @external_id, @system_id,
|
|
39
|
+
@internal_subset)
|
|
36
40
|
end
|
|
37
41
|
end
|
|
38
42
|
|
|
@@ -83,6 +87,108 @@ module Moxml
|
|
|
83
87
|
false
|
|
84
88
|
end
|
|
85
89
|
|
|
90
|
+
def add_sibling_returns_inserted?
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Linear DOCTYPE prolog scanner. Returns
|
|
95
|
+
# {name:, external_id:, system_id:, internal_subset:} or nil.
|
|
96
|
+
# Single pass, quote- and bracket-aware: no backtracking, so
|
|
97
|
+
# hostile prolog text cannot make parsing superlinear.
|
|
98
|
+
def doctype_parts(source, from = 0)
|
|
99
|
+
start = source.index("<!DOCTYPE", from)
|
|
100
|
+
return unless start
|
|
101
|
+
|
|
102
|
+
pos = skip_doctype_space(source, start + DOCTYPE_OPEN_SIZE)
|
|
103
|
+
name_start = pos
|
|
104
|
+
pos += 1 while pos < source.length &&
|
|
105
|
+
!" \t\r\n>".include?(source[pos])
|
|
106
|
+
name = source[name_start...pos]
|
|
107
|
+
return if name.empty?
|
|
108
|
+
|
|
109
|
+
pos = skip_doctype_space(source, pos)
|
|
110
|
+
external_id = nil
|
|
111
|
+
system_id = nil
|
|
112
|
+
keyword = source[pos, 6].to_s.upcase
|
|
113
|
+
if keyword == "PUBLIC"
|
|
114
|
+
pos = skip_doctype_space(source, pos + 6)
|
|
115
|
+
pair = scan_doctype_quoted(source, pos)
|
|
116
|
+
return unless pair
|
|
117
|
+
|
|
118
|
+
external_id, pos = pair
|
|
119
|
+
pos = skip_doctype_space(source, pos)
|
|
120
|
+
pair = scan_doctype_quoted(source, pos)
|
|
121
|
+
return unless pair
|
|
122
|
+
|
|
123
|
+
system_id, pos = pair
|
|
124
|
+
elsif keyword == "SYSTEM"
|
|
125
|
+
pos = skip_doctype_space(source, pos + 6)
|
|
126
|
+
pair = scan_doctype_quoted(source, pos)
|
|
127
|
+
return unless pair
|
|
128
|
+
|
|
129
|
+
system_id, pos = pair
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
pos = skip_doctype_space(source, pos)
|
|
133
|
+
internal_subset = nil
|
|
134
|
+
if source[pos] == "["
|
|
135
|
+
subset_start = pos + 1
|
|
136
|
+
pos = matching_subset_end(source, pos + 1)
|
|
137
|
+
return unless pos
|
|
138
|
+
|
|
139
|
+
internal_subset = source[subset_start...pos]
|
|
140
|
+
pos += 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
pos = skip_doctype_space(source, pos)
|
|
144
|
+
return unless source[pos] == ">"
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
name: name,
|
|
148
|
+
external_id: external_id,
|
|
149
|
+
system_id: system_id,
|
|
150
|
+
internal_subset: internal_subset,
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def skip_doctype_space(source, pos)
|
|
155
|
+
pos += 1 while pos < source.length &&
|
|
156
|
+
" \t\r\n".include?(source[pos])
|
|
157
|
+
pos
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def scan_doctype_quoted(source, pos)
|
|
161
|
+
quote = source[pos]
|
|
162
|
+
return unless QUOTES.include?(quote)
|
|
163
|
+
|
|
164
|
+
stop = source.index(quote, pos + 1)
|
|
165
|
+
return unless stop
|
|
166
|
+
|
|
167
|
+
[source[(pos + 1)...stop], stop + 1]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Index just past the closing bracket at nesting depth zero,
|
|
171
|
+
# or nil when the subset is unterminated.
|
|
172
|
+
def matching_subset_end(source, pos)
|
|
173
|
+
depth = 1
|
|
174
|
+
quote = nil
|
|
175
|
+
while pos < source.length
|
|
176
|
+
char = source[pos]
|
|
177
|
+
if quote
|
|
178
|
+
quote = nil if char == quote
|
|
179
|
+
elsif QUOTES.include?(char)
|
|
180
|
+
quote = char
|
|
181
|
+
elsif char == "["
|
|
182
|
+
depth += 1
|
|
183
|
+
elsif char == "]"
|
|
184
|
+
depth -= 1
|
|
185
|
+
return pos if depth.zero?
|
|
186
|
+
end
|
|
187
|
+
pos += 1
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
86
192
|
def parse(xml, options = {}, _context = nil)
|
|
87
193
|
# LibXML doesn't preserve DOCTYPE during parsing, so we need to extract it manually
|
|
88
194
|
xml_string = if xml.is_a?(String)
|
|
@@ -99,19 +205,24 @@ module Moxml
|
|
|
99
205
|
xml_string = preprocess_entities(xml_string)
|
|
100
206
|
|
|
101
207
|
# Extract DOCTYPE before parsing. Locate the literal first:
|
|
102
|
-
# a plain memscan
|
|
103
|
-
#
|
|
104
|
-
# and matching from the found offset keeps captures
|
|
105
|
-
# identical.
|
|
208
|
+
# a plain memscan keeps doctype-less documents (the common
|
|
209
|
+
# case) off the scanner path; the scan itself is linear.
|
|
106
210
|
doctype_idx = xml_string.index("<!DOCTYPE")
|
|
107
|
-
|
|
108
|
-
xml_string.match(DOCTYPE_RE, doctype_idx)
|
|
109
|
-
end
|
|
211
|
+
doctype = doctype_idx && doctype_parts(xml_string, doctype_idx)
|
|
110
212
|
|
|
111
213
|
native_doc = begin
|
|
112
214
|
# Handle both string and file inputs
|
|
113
215
|
parser = ::LibXML::XML::Parser.string(xml_string)
|
|
114
216
|
parser.parse
|
|
217
|
+
rescue ::ArgumentError => e
|
|
218
|
+
# Invalid byte sequences raise ArgumentError from Ruby's
|
|
219
|
+
# encoding checks before libxml sees the bytes; the
|
|
220
|
+
# facade names them (taxonomy differential, malformed
|
|
221
|
+
# class).
|
|
222
|
+
raise Moxml::ParseError.new(
|
|
223
|
+
e.message,
|
|
224
|
+
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
225
|
+
)
|
|
115
226
|
rescue ::LibXML::XML::Error => e
|
|
116
227
|
if options[:strict]
|
|
117
228
|
line = e.line
|
|
@@ -127,16 +238,17 @@ module Moxml
|
|
|
127
238
|
end
|
|
128
239
|
|
|
129
240
|
# Store DOCTYPE if found
|
|
130
|
-
if
|
|
131
|
-
name =
|
|
132
|
-
external_id =
|
|
133
|
-
system_id =
|
|
241
|
+
if doctype
|
|
242
|
+
name = doctype[:name]
|
|
243
|
+
external_id = doctype[:external_id]
|
|
244
|
+
system_id = doctype[:system_id]
|
|
134
245
|
|
|
135
246
|
doctype_wrapper = DoctypeWrapper.new(
|
|
136
247
|
native_doc,
|
|
137
248
|
name,
|
|
138
249
|
external_id,
|
|
139
250
|
system_id,
|
|
251
|
+
doctype[:internal_subset],
|
|
140
252
|
)
|
|
141
253
|
attachments.set(native_doc, :doctype, doctype_wrapper)
|
|
142
254
|
end
|
|
@@ -672,7 +784,8 @@ module Moxml
|
|
|
672
784
|
return unless node && sibling
|
|
673
785
|
|
|
674
786
|
native_node = unpatch_node(node)
|
|
675
|
-
native_sibling =
|
|
787
|
+
native_sibling = import_for_attachment(native_node,
|
|
788
|
+
unpatch_node(sibling))
|
|
676
789
|
|
|
677
790
|
# Special handling for document-level processing instructions
|
|
678
791
|
# When adding a PI as sibling to root element, store it on document
|
|
@@ -686,14 +799,35 @@ module Moxml
|
|
|
686
799
|
end
|
|
687
800
|
|
|
688
801
|
native_node.prev = native_sibling
|
|
802
|
+
native_sibling
|
|
689
803
|
end
|
|
690
804
|
|
|
691
805
|
def add_next_sibling(node, sibling)
|
|
692
806
|
return unless node && sibling
|
|
693
807
|
|
|
694
808
|
native_node = unpatch_node(node)
|
|
695
|
-
native_sibling =
|
|
809
|
+
native_sibling = import_for_attachment(native_node,
|
|
810
|
+
unpatch_node(sibling))
|
|
696
811
|
native_node.next = native_sibling
|
|
812
|
+
native_sibling
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# A markup fragment parsed by the facade lives in its own
|
|
816
|
+
# document; libxml refuses cross-document attachment. Import
|
|
817
|
+
# (deep-copy) into the target document and hand back the copy —
|
|
818
|
+
# the caller refreshes its wrapper to this native.
|
|
819
|
+
def import_for_attachment(native_node, native_sibling)
|
|
820
|
+
return native_sibling unless
|
|
821
|
+
native_node.is_a?(::LibXML::XML::Node) &&
|
|
822
|
+
native_sibling.is_a?(::LibXML::XML::Node)
|
|
823
|
+
|
|
824
|
+
target = native_node.doc
|
|
825
|
+
return native_sibling if target.nil?
|
|
826
|
+
|
|
827
|
+
source = native_sibling.doc
|
|
828
|
+
return native_sibling if source.equal?(target)
|
|
829
|
+
|
|
830
|
+
target.import(native_sibling)
|
|
697
831
|
end
|
|
698
832
|
|
|
699
833
|
def remove(node)
|
|
@@ -855,7 +989,8 @@ module Moxml
|
|
|
855
989
|
|
|
856
990
|
def processing_instruction_content(node)
|
|
857
991
|
# XML 1.0 §2.6: PI content is verbatim — no entity resolution.
|
|
858
|
-
|
|
992
|
+
# Empty data is the empty string, never nil (uniform Kind face).
|
|
993
|
+
unpatch_node(node)&.content.to_s
|
|
859
994
|
end
|
|
860
995
|
|
|
861
996
|
def set_processing_instruction_content(node, content)
|
|
@@ -936,12 +1071,12 @@ module Moxml
|
|
|
936
1071
|
|
|
937
1072
|
# libxml-ruby 5.x exposes no external/system ID accessor on
|
|
938
1073
|
# the DTD node — the DOCTYPE source text is the only source,
|
|
939
|
-
# so re-derive from it (same
|
|
1074
|
+
# so re-derive from it (same linear scanner as the parse path).
|
|
940
1075
|
def doctype_external_id(native)
|
|
941
1076
|
if native.is_a?(DoctypeWrapper)
|
|
942
1077
|
native.external_id
|
|
943
1078
|
else
|
|
944
|
-
native.to_s
|
|
1079
|
+
Libxml.doctype_parts(native.to_s)&.dig(:external_id)
|
|
945
1080
|
end
|
|
946
1081
|
end
|
|
947
1082
|
|
|
@@ -949,7 +1084,7 @@ module Moxml
|
|
|
949
1084
|
if native.is_a?(DoctypeWrapper)
|
|
950
1085
|
native.system_id
|
|
951
1086
|
else
|
|
952
|
-
|
|
1087
|
+
Libxml.doctype_parts(native.to_s)&.dig(:system_id)
|
|
953
1088
|
end
|
|
954
1089
|
end
|
|
955
1090
|
|
|
@@ -957,13 +1092,21 @@ module Moxml
|
|
|
957
1092
|
native_node = unpatch_node(node)
|
|
958
1093
|
return [] unless native_node
|
|
959
1094
|
|
|
1095
|
+
if empty_default_namespace_binding?(namespaces)
|
|
1096
|
+
return Moxml::XPath.evaluate_natives(native_node, expression,
|
|
1097
|
+
namespaces,
|
|
1098
|
+
adapter: :libxml)
|
|
1099
|
+
end
|
|
1100
|
+
|
|
960
1101
|
# libxml-ruby's Document#find returns EMPTY for relative
|
|
961
1102
|
# paths (nokogiri/libxml2 evaluate them fine): a relative
|
|
962
1103
|
# expression from the document node is equivalent to the
|
|
963
1104
|
# absolute form — the document's only name-testable child
|
|
964
1105
|
# is the root — so rewrite instead of losing the match.
|
|
1106
|
+
# Function calls and unions are not name-test paths: a
|
|
1107
|
+
# "/count(...)" rewrite is invalid XPath (scalar arm).
|
|
965
1108
|
if native_node.is_a?(::LibXML::XML::Document) &&
|
|
966
|
-
|
|
1109
|
+
relative_name_path?(expression)
|
|
967
1110
|
expression = "/#{expression}"
|
|
968
1111
|
end
|
|
969
1112
|
|
|
@@ -997,6 +1140,16 @@ module Moxml
|
|
|
997
1140
|
results&.first
|
|
998
1141
|
end
|
|
999
1142
|
|
|
1143
|
+
# A relative name-test path (the only shape the document-context
|
|
1144
|
+
# rewrite is for): a name/`*`/`@` start, no function call (the
|
|
1145
|
+
# scalar arm), no union.
|
|
1146
|
+
def relative_name_path?(expression)
|
|
1147
|
+
return false unless expression.match?(/\A[A-Za-z_*@]/)
|
|
1148
|
+
return false if expression.match?(/\A[A-Za-z][\w.-]*\s*\(/)
|
|
1149
|
+
|
|
1150
|
+
!expression.include?("|")
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1000
1153
|
# Shallow duplication: copies the node itself (name, attrs, namespaces)
|
|
1001
1154
|
# but NOT its descendants (deep duplication composes this).
|
|
1002
1155
|
# walks the source tree and re-adds children one at a time via
|
|
@@ -1019,6 +1172,7 @@ module Moxml
|
|
|
1019
1172
|
node.name,
|
|
1020
1173
|
node.external_id,
|
|
1021
1174
|
node.system_id,
|
|
1175
|
+
node.internal_subset,
|
|
1022
1176
|
)
|
|
1023
1177
|
else
|
|
1024
1178
|
node
|
|
@@ -58,6 +58,10 @@ module Moxml
|
|
|
58
58
|
true
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def element_indent_supported?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
61
65
|
# --- XSLT (Moxml::XSLT contract, libxslt-backed) ---
|
|
62
66
|
|
|
63
67
|
def xslt_compile(sheet_source)
|
|
@@ -646,6 +650,12 @@ module Moxml
|
|
|
646
650
|
end
|
|
647
651
|
|
|
648
652
|
def xpath(node, expression, namespaces = nil)
|
|
653
|
+
if empty_default_namespace_binding?(namespaces)
|
|
654
|
+
return Moxml::XPath.evaluate_natives(node, expression,
|
|
655
|
+
namespaces,
|
|
656
|
+
adapter: :nokogiri)
|
|
657
|
+
end
|
|
658
|
+
|
|
649
659
|
# An explicit bindings hash — even EMPTY — makes nokogiri
|
|
650
660
|
# register a prefix context, which disables libxml2's
|
|
651
661
|
# literal xmlns: default-namespace handling. Only pass the
|
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -28,8 +28,23 @@ module Moxml
|
|
|
28
28
|
doc.children << element
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# The UTF-8 BOM is an encoding signature, not content (model,
|
|
32
|
+
# Parsing); Oga otherwise leaves it in the tree as a
|
|
33
|
+
# document-level text node.
|
|
34
|
+
def strip_bom(xml)
|
|
35
|
+
return xml unless xml.is_a?(String)
|
|
36
|
+
|
|
37
|
+
if defined?(Encoding) && xml.encoding == Encoding::BINARY
|
|
38
|
+
xml.byteslice(0, 3) == "\xEF\xBB\xBF".b ? xml.byteslice(3..) : xml
|
|
39
|
+
elsif xml.start_with?("")
|
|
40
|
+
xml[1..]
|
|
41
|
+
else
|
|
42
|
+
xml
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
31
46
|
def parse(xml, options = {}, _context = nil)
|
|
32
|
-
processed_xml = preprocess_entities(xml)
|
|
47
|
+
processed_xml = strip_bom(preprocess_entities(xml))
|
|
33
48
|
|
|
34
49
|
native_doc = begin
|
|
35
50
|
::Oga.parse_xml(processed_xml, strict: options[:strict])
|
|
@@ -40,6 +55,18 @@ module Moxml
|
|
|
40
55
|
)
|
|
41
56
|
end
|
|
42
57
|
|
|
58
|
+
# Rootless parses (empty or declaration-only input) are
|
|
59
|
+
# malformed: XML requires exactly one root. Oga returns a
|
|
60
|
+
# bare document where nokogiri/leptris raise — the facade
|
|
61
|
+
# names it instead (loudness differential, TODO.sota/01).
|
|
62
|
+
if options[:strict] && native_doc.respond_to?(:root_node) &&
|
|
63
|
+
native_doc.children.none?(::Oga::XML::Element)
|
|
64
|
+
raise Moxml::ParseError.new(
|
|
65
|
+
"document has no root element",
|
|
66
|
+
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
43
70
|
# Oga parses multiple root elements leniently; the moxml
|
|
44
71
|
# contract requires a single root. to_a because the vendored
|
|
45
72
|
# fork's NodeSet#count ignores its block.
|
|
@@ -191,7 +218,15 @@ module Moxml
|
|
|
191
218
|
|
|
192
219
|
def namespace(element)
|
|
193
220
|
case element
|
|
194
|
-
when ::Oga::XML::
|
|
221
|
+
when ::Oga::XML::Attribute
|
|
222
|
+
# Unprefixed attributes are in no namespace, always
|
|
223
|
+
# (Namespaces 1.0 §5.2). Oga resolves them against the
|
|
224
|
+
# element's default namespace, which is wrong and makes
|
|
225
|
+
# bare-name reads miss under a default namespace.
|
|
226
|
+
return nil if element.namespace_name.nil?
|
|
227
|
+
|
|
228
|
+
element.namespace
|
|
229
|
+
when ::Oga::XML::Element
|
|
195
230
|
element.namespace
|
|
196
231
|
end
|
|
197
232
|
rescue NoMethodError
|
|
@@ -235,10 +270,10 @@ module Moxml
|
|
|
235
270
|
def children(node, **_entity_bearing)
|
|
236
271
|
all_children = []
|
|
237
272
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
# The XML declaration is document state, not a child node
|
|
274
|
+
# (model, Object model): it never appears in children listings.
|
|
275
|
+
# The doctype follows the Nokogiri contract and is listed.
|
|
276
|
+
all_children << node.doctype if node.is_a?(::Oga::XML::Document) && node.doctype
|
|
242
277
|
|
|
243
278
|
return all_children unless node.is_a?(::Oga::XML::Node) || node.is_a?(::Oga::XML::Document)
|
|
244
279
|
|
|
@@ -499,7 +534,13 @@ module Moxml
|
|
|
499
534
|
end
|
|
500
535
|
|
|
501
536
|
def processing_instruction_content(node)
|
|
502
|
-
|
|
537
|
+
# Oga keeps the target/content separator space inside the
|
|
538
|
+
# PI's text; the model's content is the data AFTER the
|
|
539
|
+
# separator. Separator whitespace is parser syntax, and
|
|
540
|
+
# leaving it in makes canonicalization non-idempotent (the
|
|
541
|
+
# serializer re-adds the separator each round — TODO.sota/02,
|
|
542
|
+
# M2).
|
|
543
|
+
node.text.to_s.sub(/\A\s+/, "")
|
|
503
544
|
end
|
|
504
545
|
|
|
505
546
|
def set_processing_instruction_content(node, content)
|
|
@@ -547,10 +588,31 @@ module Moxml
|
|
|
547
588
|
end
|
|
548
589
|
|
|
549
590
|
def xpath(node, expression, namespaces = nil)
|
|
591
|
+
# oga's engine has no xmlns: default-namespace idiom
|
|
592
|
+
# (D-01); the reference engine implements it (model,
|
|
593
|
+
# Names and namespaces).
|
|
594
|
+
if expression.include?("xmlns:")
|
|
595
|
+
return Moxml::XPath.evaluate_natives(
|
|
596
|
+
node, expression, namespaces || {}, adapter: :oga
|
|
597
|
+
)
|
|
598
|
+
end
|
|
599
|
+
|
|
550
600
|
result = node.xpath(expression, {},
|
|
551
601
|
namespaces: namespaces&.transform_keys(&:to_s))
|
|
552
|
-
# Adapter contract: Array<native> | scalar
|
|
553
|
-
|
|
602
|
+
# Adapter contract: Array<native> | scalar. Oga's engine
|
|
603
|
+
# answers node-sets in evaluation order, not document order
|
|
604
|
+
# (//* on <r><a><b/></a><c/></r> answers a,c,b); path results
|
|
605
|
+
# are document-ordered by the model — re-order here
|
|
606
|
+
# (TODO.sota/02, M3).
|
|
607
|
+
if result.is_a?(Enumerable)
|
|
608
|
+
nodes = result.to_a
|
|
609
|
+
ctx = Context.new(:oga)
|
|
610
|
+
wrapped = nodes.map { |n| Moxml::Node.wrap(n, ctx) }
|
|
611
|
+
ordered = Moxml::XPath::Ordering.document_order(wrapped)
|
|
612
|
+
ordered.map(&:native)
|
|
613
|
+
else
|
|
614
|
+
result
|
|
615
|
+
end
|
|
554
616
|
rescue ::LL::ParserError => e
|
|
555
617
|
raise Moxml::XPathError.new(
|
|
556
618
|
e.message,
|
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -60,13 +60,28 @@ module Moxml
|
|
|
60
60
|
assign_parents(doc)
|
|
61
61
|
doc
|
|
62
62
|
end
|
|
63
|
-
rescue ::Ox::ParseError => e
|
|
63
|
+
rescue ::Ox::ParseError, ::RuntimeError => e
|
|
64
|
+
# Ox leaks raw RuntimeErrors from some malformed shapes
|
|
65
|
+
# (e.g. declaration-only input reaches Element#<<); the
|
|
66
|
+
# facade names every parse-time engine failure.
|
|
64
67
|
raise Moxml::ParseError.new(
|
|
65
68
|
e.message,
|
|
66
69
|
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
67
70
|
)
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
# Rootless parses (empty or declaration-only input) are
|
|
74
|
+
# malformed: XML requires exactly one root. Ox answers a
|
|
75
|
+
# bare document — the facade names it (uniform with the
|
|
76
|
+
# rexml/oga arms; loudness differential, TODO.sota/01).
|
|
77
|
+
if options[:strict] && native_doc.is_a?(::Ox::Document) &&
|
|
78
|
+
native_doc.nodes.none?(::Ox::Element)
|
|
79
|
+
raise Moxml::ParseError.new(
|
|
80
|
+
"document has no root element",
|
|
81
|
+
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
ctx = _context || Context.new(context_adapter_name)
|
|
71
86
|
Wrappers::Document.new(native_doc, ctx)
|
|
72
87
|
end
|
|
@@ -93,6 +108,14 @@ module Moxml
|
|
|
93
108
|
end
|
|
94
109
|
end
|
|
95
110
|
|
|
111
|
+
# Ox's SAX parser reports text only: CDATA, comments and PIs
|
|
112
|
+
# are folded into text or dropped (D-10). Subscribing to those
|
|
113
|
+
# families raises the named error instead of silently answering
|
|
114
|
+
# an incomplete stream.
|
|
115
|
+
def sax_event_family_supported?(family)
|
|
116
|
+
!%i[cdata comment processing_instruction].include?(family)
|
|
117
|
+
end
|
|
118
|
+
|
|
96
119
|
def create_document(native_doc = nil)
|
|
97
120
|
attrs = native_doc&.attributes || {}
|
|
98
121
|
::Ox::Document.new(**attrs)
|
|
@@ -627,7 +650,11 @@ module Moxml
|
|
|
627
650
|
end
|
|
628
651
|
|
|
629
652
|
def processing_instruction_content(node)
|
|
630
|
-
|
|
653
|
+
# Ox keeps the target/content separator space inside the
|
|
654
|
+
# PI content; the model's content is the data AFTER the
|
|
655
|
+
# separator. Leaving it in makes canonicalization
|
|
656
|
+
# non-idempotent (TODO.sota/02, M2).
|
|
657
|
+
node.content.to_s.sub(/\A\s+/, "")
|
|
631
658
|
end
|
|
632
659
|
|
|
633
660
|
def set_processing_instruction_content(node, content)
|
|
@@ -708,6 +735,11 @@ module Moxml
|
|
|
708
735
|
end)
|
|
709
736
|
when NodeSet
|
|
710
737
|
dedupe_natives(result.to_a.map(&:native))
|
|
738
|
+
when Moxml::Node
|
|
739
|
+
# The engine answers a lone node for the context node
|
|
740
|
+
# (`.`); the adapter contract is a node-set for node
|
|
741
|
+
# results.
|
|
742
|
+
[result.native]
|
|
711
743
|
else
|
|
712
744
|
result
|
|
713
745
|
end
|