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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +7 -2
  3. data/.github/workflows/dependent-tests.yml +4 -1
  4. data/.github/workflows/nightly-fuzz.yml +80 -0
  5. data/.github/workflows/performance-release.yml +79 -0
  6. data/.github/workflows/performance.yml +59 -0
  7. data/Rakefile +23 -0
  8. data/benchmark/README.md +106 -0
  9. data/benchmark/scoreboard.json +115 -0
  10. data/benchmark/scoreboard.rb +556 -0
  11. data/bin/generate-model-coverage +21 -0
  12. data/docs/_pages/adapter-compatibility.adoc +3 -3
  13. data/docs/_pages/divergences.adoc +88 -39
  14. data/docs/_pages/ecosystem.adoc +62 -0
  15. data/docs/_pages/headed-ox-limitations.adoc +1 -1
  16. data/docs/_pages/headed-ox.adoc +2 -2
  17. data/docs/_pages/index.adoc +4 -0
  18. data/docs/_pages/model-coverage.adoc +57 -0
  19. data/docs/_pages/performance.adoc +29 -0
  20. data/docs/_pages/why-moxml.adoc +87 -0
  21. data/docs/_references/processing-model.adoc +69 -3
  22. data/lib/compat/opal/moxml_boot.rb +4 -0
  23. data/lib/moxml/adapter/base.rb +73 -0
  24. data/lib/moxml/adapter/leptris.rb +293 -23
  25. data/lib/moxml/adapter/libxml.rb +179 -25
  26. data/lib/moxml/adapter/nokogiri.rb +10 -0
  27. data/lib/moxml/adapter/oga.rb +71 -9
  28. data/lib/moxml/adapter/ox.rb +34 -2
  29. data/lib/moxml/adapter/rexml.rb +82 -17
  30. data/lib/moxml/c14n/data_model.rb +8 -0
  31. data/lib/moxml/c14n/processor.rb +17 -8
  32. data/lib/moxml/context.rb +33 -1
  33. data/lib/moxml/document.rb +18 -4
  34. data/lib/moxml/node.rb +75 -8
  35. data/lib/moxml/node_set.rb +42 -1
  36. data/lib/moxml/reference/normalizer.rb +63 -0
  37. data/lib/moxml/reference/parser.rb +417 -0
  38. data/lib/moxml/reference/writer.rb +121 -0
  39. data/lib/moxml/reference.rb +205 -0
  40. data/lib/moxml/relaxng.rb +62 -0
  41. data/lib/moxml/sax/block_handler.rb +7 -0
  42. data/lib/moxml/sax/handler.rb +20 -0
  43. data/lib/moxml/version.rb +1 -1
  44. data/lib/moxml/xpath/compiler.rb +17 -2
  45. data/lib/moxml/xpath/ordering.rb +97 -0
  46. data/lib/moxml/xpath.rb +29 -0
  47. data/lib/moxml.rb +2 -0
  48. data/spec/consistency/adapter_parity_spec.rb +24 -0
  49. data/spec/consistency/citation_hygiene_spec.rb +30 -0
  50. data/spec/consistency/differential_taxonomy_spec.rb +117 -0
  51. data/spec/consistency/divergence_register_spec.rb +9 -1
  52. data/spec/consistency/eol_normalization_spec.rb +2 -0
  53. data/spec/consistency/loud_degradation_spec.rb +2 -0
  54. data/spec/consistency/metamorphic_properties_spec.rb +195 -0
  55. data/spec/consistency/metamorphic_spec.rb +138 -0
  56. data/spec/consistency/model_coverage_audit_spec.rb +144 -61
  57. data/spec/consistency/published_docs_spec.rb +49 -0
  58. data/spec/consistency/reference_differential_spec.rb +186 -0
  59. data/spec/consistency/xpath_idioms_spec.rb +34 -5
  60. data/spec/consistency/xslt_spec.rb +2 -0
  61. data/spec/integration/shared_examples/edge_cases.rb +0 -13
  62. data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
  63. data/spec/moxml/adapter/leptris_spec.rb +23 -3
  64. data/spec/moxml/adapter/ox_spec.rb +1 -1
  65. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
  66. data/spec/moxml/attribute_resolver_spec.rb +35 -0
  67. data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -6
  68. data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
  69. data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
  70. data/spec/moxml/c14n/subset_spec.rb +2 -12
  71. data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
  72. data/spec/moxml/doctype_spec.rb +7 -0
  73. data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
  74. data/spec/moxml/element_indent_spec.rb +34 -0
  75. data/spec/moxml/entity_spec.rb +2 -0
  76. data/spec/moxml/node_spec.rb +0 -3
  77. data/spec/moxml/plan_conformance_spec.rb +2 -0
  78. data/spec/moxml/processing_instruction_spec.rb +9 -0
  79. data/spec/moxml/reference_opal_spec.rb +29 -0
  80. data/spec/moxml/reference_spec.rb +162 -0
  81. data/spec/moxml/relax_ng_spec.rb +67 -0
  82. data/spec/moxml/sax_spec.rb +47 -74
  83. data/spec/moxml/text_spec.rb +2 -0
  84. data/spec/moxml/to_html_spec.rb +2 -0
  85. data/spec/moxml/xhtml_output_spec.rb +2 -0
  86. data/spec/moxml/xpath/axes_spec.rb +2 -2
  87. data/spec/moxml/xpath_capabilities_spec.rb +11 -38
  88. data/spec/moxml/xsd_spec.rb +38 -0
  89. data/spec/support/input_generator.rb +154 -0
  90. data/spec/support/input_taxonomy.rb +219 -0
  91. data/spec/support/model_coverage.rb +159 -0
  92. data/spec/support/xml_generator.rb +54 -0
  93. metadata +32 -2
@@ -81,9 +81,7 @@ RSpec.describe "XPath Capabilities" do
81
81
  it "supports attribute value matching ([@attr='value'])" do
82
82
  doc = Moxml.new.parse(simple_xml)
83
83
  programming = doc.xpath("//book[@category='programming']")
84
- expect(programming.length).to be >= 0 # May not work on all
85
- rescue Moxml::XPathError
86
- skip "D-12: Attribute value predicates not supported on #{adapter_name}"
84
+ expect(programming.length).to eq(1)
87
85
  end
88
86
  end
89
87
 
@@ -96,9 +94,7 @@ RSpec.describe "XPath Capabilities" do
96
94
  }
97
95
 
98
96
  items = doc.xpath("//book:item", namespaces)
99
- expect(items.length).to be >= 0
100
- rescue Moxml::XPathError, NoMethodError
101
- skip "D-12: Namespace XPath not supported on #{adapter_name}"
97
+ expect(items.length).to eq(2)
102
98
  end
103
99
 
104
100
  it "handles default namespaces" do
@@ -106,9 +102,7 @@ RSpec.describe "XPath Capabilities" do
106
102
  namespaces = { "lib" => "http://library.org" }
107
103
 
108
104
  library = doc.xpath("//lib:library", namespaces)
109
- expect(library.length).to be >= 0
110
- rescue Moxml::XPathError, NoMethodError
111
- skip "D-12: Default namespace XPath not supported on #{adapter_name}"
105
+ expect(library.length).to eq(1)
112
106
  end
113
107
  end
114
108
 
@@ -116,17 +110,13 @@ RSpec.describe "XPath Capabilities" do
116
110
  it "supports numeric positions ([1])" do
117
111
  doc = Moxml.new.parse(simple_xml)
118
112
  first_book = doc.xpath("//book[1]")
119
- expect(first_book.length).to be >= 0
120
- rescue Moxml::XPathError
121
- skip "D-12: Position predicates not supported on #{adapter_name}"
113
+ expect(first_book.length).to eq(1)
122
114
  end
123
115
 
124
116
  it "supports position() function" do
125
117
  doc = Moxml.new.parse(simple_xml)
126
118
  first_books = doc.xpath("//book[position()=1]")
127
- expect(first_books.length).to be >= 0
128
- rescue Moxml::XPathError
129
- skip "D-12: position() function not supported on #{adapter_name}"
119
+ expect(first_books.length).to eq(1)
130
120
  end
131
121
  end
132
122
 
@@ -134,20 +124,13 @@ RSpec.describe "XPath Capabilities" do
134
124
  it "supports count() function, returning the scalar unwrapped" do
135
125
  doc = Moxml.new.parse(simple_xml)
136
126
  result = doc.xpath("count(//book)")
137
- # REXML's XPath has no function support and yields an empty set
138
- skip "D-12: count() function not supported on #{adapter_name}" if result.is_a?(Moxml::NodeSet)
139
-
140
127
  expect(result).to eq(2)
141
- rescue Moxml::XPathError, NoMethodError
142
- skip "D-12: count() function not supported on #{adapter_name}"
143
128
  end
144
129
 
145
130
  it "supports text() node selection" do
146
131
  doc = Moxml.new.parse(simple_xml)
147
132
  texts = doc.xpath("//title/text()")
148
- expect(texts.length).to be >= 0
149
- rescue Moxml::XPathError
150
- skip "D-12: text() not supported on #{adapter_name}"
133
+ expect(texts.length).to eq(3)
151
134
  end
152
135
  end
153
136
 
@@ -155,17 +138,13 @@ RSpec.describe "XPath Capabilities" do
155
138
  it "supports numeric comparisons ([@attr < value])" do
156
139
  doc = Moxml.new.parse(simple_xml)
157
140
  cheap_books = doc.xpath("//book[@id < 2]")
158
- expect(cheap_books.length).to be >= 0
159
- rescue Moxml::XPathError
160
- skip "D-12: Numeric predicates not supported on #{adapter_name}"
141
+ expect(cheap_books.length).to eq(1)
161
142
  end
162
143
 
163
144
  it "supports boolean expressions ([cond1 and cond2])" do
164
145
  doc = Moxml.new.parse(simple_xml)
165
146
  result = doc.xpath("//book[@id and @category]")
166
- expect(result.length).to be >= 0
167
- rescue Moxml::XPathError
168
- skip "D-12: Boolean expressions not supported on #{adapter_name}"
147
+ expect(result.length).to eq(2)
169
148
  end
170
149
  end
171
150
 
@@ -173,17 +152,13 @@ RSpec.describe "XPath Capabilities" do
173
152
  it "supports child axis" do
174
153
  doc = Moxml.new.parse(simple_xml)
175
154
  children = doc.xpath("/library/child::book")
176
- expect(children.length).to be >= 0
177
- rescue Moxml::XPathError
178
- skip "D-12: child:: axis not supported on #{adapter_name}"
155
+ expect(children.length).to eq(2)
179
156
  end
180
157
 
181
158
  it "supports descendant axis" do
182
159
  doc = Moxml.new.parse(simple_xml)
183
160
  descendants = doc.xpath("//library/descendant::title")
184
- expect(descendants.length).to be >= 0
185
- rescue Moxml::XPathError
186
- skip "D-12: descendant:: axis not supported on #{adapter_name}"
161
+ expect(descendants.length).to eq(3)
187
162
  end
188
163
  end
189
164
 
@@ -191,9 +166,7 @@ RSpec.describe "XPath Capabilities" do
191
166
  it "supports union (|)" do
192
167
  doc = Moxml.new.parse(simple_xml)
193
168
  items = doc.xpath("//book | //magazine")
194
- expect(items.length).to be >= 0
195
- rescue Moxml::XPathError
196
- skip "D-12: Union operator not supported on #{adapter_name}"
169
+ expect(items.length).to eq(3)
197
170
  end
198
171
  end
199
172
  end
@@ -141,4 +141,42 @@ RSpec.describe Moxml::XSD do
141
141
  .to eq(Moxml::Adapter::Leptris::NATIVE_XSD)
142
142
  end
143
143
  end
144
+
145
+ describe "content_valid? with C-minted identity children (moxml#410)",
146
+ if: Moxml::Adapter::Leptris::XSD_SUPPORTED do
147
+ let(:ctx) { Moxml.new(:leptris) }
148
+ let(:schema_text) do
149
+ <<~XML
150
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
151
+ <xs:element name="person">
152
+ <xs:complexType>
153
+ <xs:sequence>
154
+ <xs:element name="name" type="xs:string"/>
155
+ </xs:sequence>
156
+ </xs:complexType>
157
+ </xs:element>
158
+ </xs:schema>
159
+ XML
160
+ end
161
+
162
+ it "answers for identity wrappers minted at create (1.9.374+)" do
163
+ schema = ctx.xsd(schema_text)
164
+ doc = ctx.create_document
165
+ root = doc.create_element("person")
166
+ doc.root = root
167
+ name = doc.create_element("name")
168
+ name.text = "A"
169
+ root.add_child(name)
170
+
171
+ expect(root.native.class.name).to include("Identity")
172
+ expect(schema.content_valid?("person", root.children.to_a)).to be(true)
173
+ end
174
+
175
+ it "answers for identity children from parse" do
176
+ schema = ctx.xsd(schema_text)
177
+ root = ctx.parse("<person><name>A</name></person>").root
178
+ expect(schema.content_valid?("person", root.children.to_a)).to be(true)
179
+ expect(schema.content_valid?("person", [])).to be(false)
180
+ end
181
+ end
144
182
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Seeded XML input generator (TODO.sota/02): composes the 01
4
+ # taxonomy's vocabulary into fresh inputs, deterministically from a
5
+ # seed. CI legs run a fixed seed — identical cases every run; the
6
+ # nightly fuzz leg passes a random one (MOXML_FUZZ_SEED). Every case
7
+ # id carries its provenance ("gen <seed>/<i> <malform>"), so a
8
+ # failure reproduces at that seed and index.
9
+ module InputGenerator
10
+ # Generated inputs record the divergence-relevant features they use
11
+ # so the differential exempts exactly those cells, never silently —
12
+ # every value is a register ID (docs/_pages/divergences.adoc).
13
+ FEATURE_EXEMPT = {
14
+ inter_element_ws: {
15
+ nodes: { ox: "D-13", headed_ox: "D-13" },
16
+ text: { ox: "D-13", headed_ox: "D-13" },
17
+ },
18
+ pi_in_content: { text: { oga: "D-14" } },
19
+ pre_root_ws: { nodes: { rexml: "D-15" }, text: { rexml: "D-15" } },
20
+ adjacent_cdata: {
21
+ nodes: { oga: "D-19", rexml: "D-19", ox: "D-19", headed_ox: "D-19",
22
+ leptris: "D-19" },
23
+ },
24
+ }.freeze
25
+
26
+ # malform is one of the three guaranteed-breaking structural
27
+ # mutations; oga's strict lane is lexer-strict (D-17) and is
28
+ # exempted for all of them in the strict-raise property.
29
+ Case = Struct.new(:id, :xml, :well_formed, :features, :voids, :malform)
30
+
31
+ ELEMENTS = %w[a b sec p].freeze
32
+ VOIDS = %w[br img hr].freeze
33
+ ATTR_VALUES = [
34
+ "1", "x y", "a &amp; b", "&lt;tag&gt;", "&#65;&#x42;", "taillé"
35
+ ].freeze
36
+ TEXTS = [
37
+ "plain text", " spaced ", "a &amp; b &lt; c", "&#65;&#x42;",
38
+ "café &#160; NBSP", "word"
39
+ ].freeze
40
+ NAMESPACES = [
41
+ 'xmlns="urn:t"', 'xmlns:ns="urn:n"', 'xmlns="urn:r"'
42
+ ].freeze
43
+ XML_ATTRS = ['xml:space="preserve"', 'xml:lang="en"'].freeze
44
+ MALFORMS = %i[unclosed stray attr_quote].freeze
45
+
46
+ module_function
47
+
48
+ # mode: :fuzz mixes the three structural malformations into roughly
49
+ # a third of the cases (the strict-raise property consumes those);
50
+ # :well_formed answers pure compositions.
51
+ def cases(seed:, count:, mode: :fuzz)
52
+ rng = Random.new(seed)
53
+ Array.new(count) do |i|
54
+ body, features, voids = compose(rng)
55
+ id = "gen #{seed}/#{i}"
56
+ if mode == :fuzz && rng.rand(3).zero?
57
+ malform = MALFORMS[rng.rand(MALFORMS.length)]
58
+ Case.new("#{id} #{malform}", mutate(body, malform), false,
59
+ features, voids, malform)
60
+ else
61
+ Case.new(id, body, true, features, voids, nil)
62
+ end
63
+ end
64
+ end
65
+
66
+ def compose(rng)
67
+ features = []
68
+ voids = []
69
+ pre = +""
70
+ draw = rng.rand(6)
71
+ if draw.zero?
72
+ pre << "<!--pre-root-->"
73
+ elsif draw == 1
74
+ pre << "<?prep root?>"
75
+ elsif draw == 2
76
+ pre << " "
77
+ features << :pre_root_ws
78
+ end
79
+
80
+ attrs = +'<r id="root"'
81
+ attrs << " #{NAMESPACES[rng.rand(NAMESPACES.length)]}" if rng.rand(2).zero?
82
+ attrs << " #{XML_ATTRS[rng.rand(XML_ATTRS.length)]}" if rng.rand(3).zero?
83
+ attrs << ' ns:x="v"' if rng.rand(3).zero?
84
+ attrs << ">"
85
+
86
+ body = +""
87
+ depth = 1 + rng.rand(4)
88
+ (1 + rng.rand(4)).times { body << child(rng, 0, depth, features, voids) }
89
+ # a whitespace-only run between two constructs is a dropped-text
90
+ # divergence cell (D-13 on ox/headed_ox)
91
+ features << :inter_element_ws if body.match?(%r{>\s+<})
92
+
93
+ xml = "#{pre}#{attrs}#{body}</r>"
94
+ # a default-namespace declaration scopes XPath name tests away
95
+ # from generated a/b elements (XPath 1.0, processing model
96
+ # "Names and namespaces")
97
+ features << :default_ns if xml.include?('xmlns="')
98
+ # adjacent text-like leaves — CDATA next to CDATA/text — coalesce
99
+ # on the libxml2 lane (D-19)
100
+ if xml.match?(/]\]>\s*<!\[CDATA\[/) || xml.match?(/]\]>\s*[^<\s]/) ||
101
+ xml.match?(/[^<\s]\s*<!\[CDATA\[/)
102
+ features << :adjacent_cdata
103
+ end
104
+ [xml, features.uniq, voids.uniq]
105
+ end
106
+
107
+ def child(rng, level, max_depth, features, voids)
108
+ if level < max_depth && rng.rand(8) < 5
109
+ inner = +""
110
+ (1 + rng.rand(3)).times do
111
+ inner << child(rng, level + 1, max_depth, features, voids)
112
+ end
113
+ name = ELEMENTS[rng.rand(ELEMENTS.length)]
114
+ extra = +""
115
+ extra << " #{NAMESPACES[rng.rand(NAMESPACES.length)]}" if rng.rand(4).zero?
116
+ extra << " #{XML_ATTRS[rng.rand(XML_ATTRS.length)]}" if rng.rand(4).zero?
117
+ extra << " ns:x=\"#{ATTR_VALUES[rng.rand(ATTR_VALUES.length)]}\"" if rng.rand(3).zero?
118
+ "<#{name}#{extra}>#{inner}</#{name}>"
119
+ else
120
+ leaf(rng, features, voids)
121
+ end
122
+ end
123
+
124
+ def leaf(rng, features, voids)
125
+ case rng.rand(6)
126
+ when 0, 1
127
+ TEXTS[rng.rand(TEXTS.length)]
128
+ when 2
129
+ "<!--comment #{rng.rand(9)}-->"
130
+ when 3
131
+ features << :pi_in_content
132
+ "<?inst data-#{rng.rand(9)}?>"
133
+ when 4
134
+ "<![CDATA[x < #{rng.rand(9)} & more]]>"
135
+ else
136
+ v = VOIDS[rng.rand(VOIDS.length)]
137
+ voids << v
138
+ "<#{v}/>"
139
+ end
140
+ end
141
+
142
+ # Each mutation is guaranteed to break well-formedness (the root
143
+ # close and the root id attribute always exist in composed bodies).
144
+ def mutate(xml, malform)
145
+ case malform
146
+ when :unclosed
147
+ xml.sub(%r{</[a-z]+>}, "")
148
+ when :stray
149
+ "#{xml}</r>"
150
+ when :attr_quote
151
+ xml.sub('id="root"', 'id="root')
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The canonical input-class taxonomy (TODO.sota/01-totality): every
4
+ # class of input the processing model touches, as shared fixture data
5
+ # the differential suites consume. Adding a class here automatically
6
+ # extends the whole-class differential; a class without a stable
7
+ # cross-adapter answer must cite a registered divergence (D-ID) or a
8
+ # named raise in the consuming suite.
9
+ module InputTaxonomy
10
+ Input = Struct.new(:id, :klass, :xml, :well_formed, :note) do
11
+ def well_formed? = well_formed
12
+ end
13
+
14
+ WELL_FORMED = "well-formed"
15
+ MALFORMED = "malformed"
16
+
17
+ def self.all
18
+ @all ||= inputs.freeze
19
+ end
20
+
21
+ def self.well_formed
22
+ all.select(&:well_formed?)
23
+ end
24
+
25
+ def self.malformed
26
+ all.reject(&:well_formed?)
27
+ end
28
+
29
+ def self.by_klass(klass)
30
+ all.select { |i| i.klass == klass }
31
+ end
32
+
33
+ CLASSES = [
34
+ "empty/minimal",
35
+ "deep nesting",
36
+ "huge text/attributes",
37
+ "entities",
38
+ "namespaces",
39
+ "mixed content",
40
+ "pi/comment/cdata edges",
41
+ "malformed",
42
+ ].freeze
43
+
44
+ # Registered per-input exemptions for the cross-adapter
45
+ # differential: input id => { face => { adapter => D-NN } }. The
46
+ # values are register IDs (docs/_pages/divergences.adoc) enforced
47
+ # by the coverage audit.
48
+ EXEMPT = {
49
+ "mixed content: whitespace runs" => {
50
+ nodes: { ox: "D-13", headed_ox: "D-13" },
51
+ text: { ox: "D-13", headed_ox: "D-13" },
52
+ },
53
+ "mixed content: CRLF text normalizes to LF" => {
54
+ text: { oga: "D-02" },
55
+ },
56
+ "mixed content: bare CR text normalizes to LF" => {
57
+ text: { oga: "D-02" },
58
+ },
59
+ "mixed content: character-ref CR survives" => {
60
+ text: { oga: "D-02", ox: "D-03", headed_ox: "D-03" },
61
+ },
62
+ "pi/comment/cdata: pi inside content" => {
63
+ text: { oga: "D-14" },
64
+ pi_content: { ox: "D-05", headed_ox: "D-05" },
65
+ },
66
+ "pi/comment/cdata: pi multi-space separator" => {
67
+ text: { oga: "D-14" },
68
+ pi_content: { ox: "D-05", headed_ox: "D-05" },
69
+ },
70
+ "pi/comment/cdata: pi trailing whitespace" => {
71
+ text: { oga: "D-14" },
72
+ pi_content: { ox: "D-05", headed_ox: "D-05", rexml: "D-20" },
73
+ },
74
+ "huge text with entities" => {
75
+ text: { rexml: "D-16" },
76
+ serialize: { rexml: "D-16" },
77
+ },
78
+ "pi/comment/cdata: pi before root" => {
79
+ nodes: { rexml: "D-15" },
80
+ text: { rexml: "D-15" },
81
+ pi_content: { ox: "D-05", headed_ox: "D-05" },
82
+ },
83
+ "pi/comment/cdata: comment before and after root" => {
84
+ comment_content: { ox: "D-06", headed_ox: "D-06" },
85
+ },
86
+ "pi/comment/cdata: comment inside content" => {
87
+ comment_content: { ox: "D-06", headed_ox: "D-06" },
88
+ },
89
+ }.freeze
90
+
91
+ # Engines whose strict lane cannot see a class of malformedness are
92
+ # registered: oga's strict is lexer-strict, not well-formedness-
93
+ # strict (D-17). leptris raises through the facade guard (D-18,
94
+ # leptris-ruby#514) and is therefore verified, not exempt; entity
95
+ # and prefix classes are engine-variant by design (entity modes,
96
+ # namespace validation).
97
+ STRICT_EXEMPT = {
98
+ "malformed: bad nesting" => { oga: "D-17" },
99
+ "malformed: invalid utf-8 byte" => { oga: "D-18", ox: "D-18",
100
+ headed_ox: "D-18" },
101
+ }.freeze
102
+
103
+ def self.inputs
104
+ [
105
+ # -- empty/minimal --
106
+ Input.new("empty/minimal: empty string", "empty/minimal", "", false,
107
+ "no root: parse must fail loudly or per contract"),
108
+ Input.new("empty/minimal: whitespace only", "empty/minimal", " \n\t ", false,
109
+ "whitespace-only: no root"),
110
+ Input.new("empty/minimal: bare root", "empty/minimal", "<r/>", true, nil),
111
+ Input.new("empty/minimal: empty paired root", "empty/minimal", "<r></r>", true, nil),
112
+ Input.new("empty/minimal: declaration only", "empty/minimal",
113
+ '<?xml version="1.0"?>', false, "declaration without root"),
114
+
115
+ # -- deep nesting --
116
+ Input.new("deep nesting: 200 levels", "deep nesting",
117
+ "<r>#{'<d>' * 200}leaf#{'</d>' * 200}</r>", true, nil),
118
+ Input.new("deep nesting: siblings at depth", "deep nesting",
119
+ "<r>#{'<a><b>' * 50}#{'</b></a>' * 50}</r>", true, nil),
120
+
121
+ # -- huge text/attributes --
122
+ Input.new("huge text: 100k chars", "huge text/attributes",
123
+ "<r>#{'x' * 100_000}</r>", true, nil),
124
+ Input.new("huge attribute: 10k chars", "huge text/attributes",
125
+ %(<r a="#{'y' * 10_000}"/>), true, nil),
126
+ Input.new("huge text with entities", "huge text/attributes",
127
+ "<r>#{'&amp;' * 20_000}</r>", true, nil),
128
+
129
+ # -- entities: built-in --
130
+ Input.new("entities: all five built-ins", "entities",
131
+ "<r>&amp;&lt;&gt;&quot;&apos;</r>", true, nil),
132
+ # -- entities: numeric --
133
+ Input.new("entities: decimal numeric", "entities",
134
+ "<r>&#65;&#233;</r>", true, nil),
135
+ Input.new("entities: hex numeric", "entities",
136
+ "<r>&#x41;&#xE9;</r>", true, nil),
137
+ # -- entities: custom (DTD-declared) --
138
+ Input.new("entities: custom DTD entity", "entities",
139
+ '<!DOCTYPE r [<!ENTITY x "expanded">]><r>&x;</r>', true, nil),
140
+ # -- entities: undefined (disabled context) --
141
+ Input.new("entities: undefined reference", "entities",
142
+ "<r>&undefinedEntity;</r>", false,
143
+ "undefined entity: malformed without a DTD"),
144
+ Input.new("entities: NBSP char ref", "entities",
145
+ "<r>&#160;</r>", true, "U+00A0 survives as content"),
146
+
147
+ # -- namespaces: default --
148
+ Input.new("namespaces: default", "namespaces",
149
+ '<r xmlns="urn:a"><c/></r>', true, nil),
150
+ # -- namespaces: prefixed --
151
+ Input.new("namespaces: prefixed", "namespaces",
152
+ '<r xmlns:p="urn:a"><p:c/></r>', true, nil),
153
+ # -- namespaces: undeclared prefix --
154
+ Input.new("namespaces: undeclared prefix", "namespaces",
155
+ "<p:c/>", false,
156
+ "undeclared prefix: malformed in strict parsing"),
157
+ # -- namespaces: redeclared --
158
+ Input.new("namespaces: redeclared", "namespaces",
159
+ '<r xmlns:p="urn:a"><s xmlns:p="urn:b"><p:c/></s></r>', true, nil),
160
+ # -- namespaces: reserved xml --
161
+ Input.new("namespaces: reserved xml prefix", "namespaces",
162
+ '<r xml:lang="en"/>', true, nil),
163
+ # -- namespaces: xmlns attribute --
164
+ Input.new("namespaces: xmlns declaration attribute", "namespaces",
165
+ '<r xmlns="urn:a" xmlns:p="urn:b"/>', true, nil),
166
+
167
+ # -- mixed content --
168
+ Input.new("mixed content: text and inline", "mixed content",
169
+ "<r>text <b>bold</b> tail</r>", true, nil),
170
+ Input.new("mixed content: whitespace runs", "mixed content",
171
+ "<r>\n <b>x</b>\n <b>y</b>\n</r>", true, nil),
172
+ Input.new("mixed content: attribute entities in text", "mixed content",
173
+ '<r a="&lt;&amp;">&lt;tag&gt;</r>', true, nil),
174
+ Input.new("mixed content: CRLF text normalizes to LF", "mixed content",
175
+ "<r>a\r\nb</r>", true, "XML 1.0 §2.11"),
176
+ Input.new("mixed content: bare CR text normalizes to LF", "mixed content",
177
+ "<r>a\rb</r>", true, "XML 1.0 §2.11"),
178
+ Input.new("mixed content: character-ref CR survives", "mixed content",
179
+ "<r>a&#xD;b</r>", true, "&#xD; is not normalized"),
180
+
181
+ # -- pi/comment/cdata edges --
182
+ Input.new("pi/comment/cdata: pi before root", "pi/comment/cdata edges",
183
+ '<?xml-stylesheet href="a"?> <r/>', true, nil),
184
+ Input.new("pi/comment/cdata: comment before and after root", "pi/comment/cdata edges",
185
+ "<!-- lead --><r/><!-- trail -->", true, nil),
186
+ Input.new("pi/comment/cdata: cdata with markup", "pi/comment/cdata edges",
187
+ "<r><![CDATA[<notmarkup> & raw]]></r>", true, nil),
188
+ Input.new("pi/comment/cdata: empty cdata", "pi/comment/cdata edges",
189
+ "<r><![CDATA[]]></r>", true, nil),
190
+ Input.new("pi/comment/cdata: comment inside content", "pi/comment/cdata edges",
191
+ "<r>a<!-- mid -->b</r>", true, nil),
192
+ Input.new("pi/comment/cdata: pi inside content", "pi/comment/cdata edges",
193
+ "<r>a<?pi data?>b</r>", true,
194
+ "PI content must not contribute to text (D-14 on oga)"),
195
+ Input.new("pi/comment/cdata: pi multi-space separator", "pi/comment/cdata edges",
196
+ "<r><?pi data?></r>", true,
197
+ "separator whitespace is not PI content (uniform)"),
198
+ Input.new("pi/comment/cdata: pi trailing whitespace", "pi/comment/cdata edges",
199
+ "<r><?pi data ?></r>", true,
200
+ "trailing whitespace is PI data (D-20 on rexml)"),
201
+ Input.new("pi/comment/cdata: empty pi", "pi/comment/cdata edges",
202
+ "<r><?pi?></r>", true, nil),
203
+
204
+ # -- malformed: unclosed --
205
+ Input.new("malformed: unclosed element", "malformed", "<r><c>", false, nil),
206
+ # -- malformed: bad nest --
207
+ Input.new("malformed: bad nesting", "malformed", "<r><a><b></a></b></r>", false, nil),
208
+ # -- malformed: bad attribute --
209
+ Input.new("malformed: unterminated attribute", "malformed",
210
+ '<r a="x>', false, nil),
211
+ # -- malformed: stray closing --
212
+ Input.new("malformed: stray closing tag", "malformed", "</r>", false, nil),
213
+ # -- malformed: invalid encoding bytes --
214
+ Input.new("malformed: invalid utf-8 byte", "malformed",
215
+ "<r>\xC3\x28</r>", false,
216
+ "0xC3 0x28 is not valid UTF-8"),
217
+ ].freeze
218
+ end
219
+ end