moxml 0.5.129 → 0.5.131

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f040a5d5fc3e041e15a54cc42aeb10ca5d3cc882ecc22ca97faa7d45cbbb2d5
4
- data.tar.gz: de59fd55dd4eff18451ddec5a20f246325be236313a55d0aeb4cab5af56ce4f9
3
+ metadata.gz: 2de6081e549de3acc9fe0fe2697d0f44e137673eaa311dc9beec4de6346dfb54
4
+ data.tar.gz: 026a76efc77132825449057e48559626e3f3ec4166d54fb5ea817d51accb830e
5
5
  SHA512:
6
- metadata.gz: 668587c18c1bf04415c99e715460b1dece9f5619206d6a7fb1ffa8260a32be81f6faa20853eeca322b3b016523a589550711647362f03ff79f64ccc507ef8e03
7
- data.tar.gz: 7ff1f188edb97ee6f36c0294745232f1e1a4078bd6e94d567d56e649c01f5939617b345ebed2fe5b21bf325b6be1863f68b5af1acfb7171bd63b0a4bef58bd2a
6
+ metadata.gz: 369b3b0ca0527711f87e4b18f9b268c503612c9aed6aa8ea4fcfcb6dd9ad835be5cbb80b9ff00b0606b3df908bb150a0ef22bba6041157bfdb41a0bd8264a153
7
+ data.tar.gz: a86491ce96b541420ddf36fc2cbe1324c2f283d212d0271cb2eb61129bca15258dd7d92dbe7a022c6e3e4055140fb395111c96e8518ebd5b5d882a036186ae33
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
- gem "leptris", "~> 1.9.343"
12
+ gem "leptris", "~> 1.9.345"
13
13
  gem "libxml-ruby", ">= 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
@@ -23,6 +23,18 @@ module Moxml
23
23
  CHILDREN_CAPACITY = 64
24
24
  private_constant :CHILDREN_CAPACITY
25
25
 
26
+ # #367: HTML-parsed trees carry no namespace declarations,
27
+ # so the engine's attribute-URI face answers nil for the
28
+ # RESERVED prefixes too (xml:lang loses its identity). The
29
+ # wrapper path resolves them from the builtin table
30
+ # (AttributeResolver) — the bulk stride mirrors that, so
31
+ # records and wrapper API agree on every tree flavor.
32
+ RESERVED_ATTR_PREFIX_URIS = {
33
+ "xml" => "http://www.w3.org/XML/1998/namespace",
34
+ "xmlns" => "http://www.w3.org/2000/xmlns/",
35
+ }.freeze
36
+ private_constant :RESERVED_ATTR_PREFIX_URIS
37
+
26
38
  # Bulk materialization (issues #132/#143): a Ruby-side
27
39
  # post-order recursion over RAW C pointers — no binding
28
40
  # wrapper per node, no per-node FFI callback, no depth memo
@@ -132,6 +144,7 @@ module Moxml
132
144
  # path's resolver semantics (moxml's canonical shape).
133
145
  attr_name = attr_name[(colon + 1)..]
134
146
  attr_uri = f.leptris_attribute_namespace_uri(attr)
147
+ attr_uri ||= RESERVED_ATTR_PREFIX_URIS[attr_prefix]
135
148
  else
136
149
  attr_prefix = nil
137
150
  attr_uri = nil
@@ -182,6 +182,18 @@ module Moxml
182
182
  defined?(::Leptris::HTML) &&
183
183
  ::Leptris::HTML.respond_to?(:parse_fragment)
184
184
 
185
+ # The engine's recovery mode (moxml#369, engine leptris#1660
186
+ # via binding 1.9.344.0): non-strict parses pass recover:
187
+ # true and keep the best-effort tree (libxml2
188
+ # XML_PARSE_RECOVER semantics — mismatched closers close
189
+ # intermediates, unclosed elements close at EOF) instead of
190
+ # the rescue-to-empty fallback. The kwarg probe pairs the
191
+ # floor (a binding without the kwarg cannot arm).
192
+ RECOVER_MODE =
193
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.344.0") &&
194
+ ::Leptris::XML::ParseOptions.instance_method(:initialize)
195
+ .parameters.any? { |_, key| key == :recover }
196
+
185
197
  # Markup-accumulating builder face (leptris-ruby#374 lever 2,
186
198
  # binding 1.9.313): the DSL accumulates markup in a plain
187
199
  # String and ONE native crossing (parse + attach) flushes the
@@ -1031,23 +1043,45 @@ module Moxml
1031
1043
  # multi-pass consumers (comparison, diff, signature).
1032
1044
  # dtdattr: true opts into DTD ATTLIST default materialization
1033
1045
  # (off by default since libleptris 1.9.8, matching libxml2).
1046
+ # #369: non-strict parses RECOVER on engines that carry
1047
+ # the mode (1.9.344+) — the best-effort tree survives
1048
+ # recoverable structural errors (libxml2
1049
+ # XML_PARSE_RECOVER semantics). Strict callers never
1050
+ # recover: malformed input raises. Direct adapter calls
1051
+ # are caller-owned-options by contract (Context applies
1052
+ # config.strict_parsing; bypassing it means the caller
1053
+ # chose the mode).
1054
+ recover = !options[:strict] && RECOVER_MODE
1034
1055
  native_doc = begin
1035
1056
  ::Leptris::XML::Document.parse(
1036
1057
  processed,
1037
1058
  readonly: options[:readonly] == true,
1038
- options: parse_flags(options, context: _context),
1059
+ options: parse_flags(options, recover: recover,
1060
+ context: _context),
1039
1061
  )
1040
1062
  rescue ::Leptris::XML::ParseError => e
1041
- # libleptris has no recovery mode that survives unclosed
1042
- # tags; non-strict callers get an empty document, matching
1043
- # the Libxml adapter's non-strict behavior. The fatal
1044
- # error rides the document as parse diagnostics (issue
1045
- # #147) — otherwise nothing says why it came back empty.
1046
1063
  raise Moxml::ParseError.new(e.message) if options[:strict]
1047
1064
 
1065
+ # Engines below the 1.9.344 recover floor (or the
1066
+ # residual unrecoverable edges, e.g. trailing content
1067
+ # after the root close) have no best-effort tree: the
1068
+ # empty-document fallback stands, with the fatal error
1069
+ # as parse diagnostics (issue #147) — otherwise nothing
1070
+ # says why it came back empty.
1048
1071
  recover_errors = [e.message]
1049
1072
  create_document
1050
1073
  end
1074
+ if recover && native_doc.root.nil? && !recover_errors
1075
+ # The recover path answers a rootless document for
1076
+ # input with nothing to recover into (whitespace-only,
1077
+ # declaration-only) — libxml2's recover behaves the
1078
+ # same. Surface WHY on parse_errors so consumers report
1079
+ # it instead of silently holding an empty tree.
1080
+ recover_errors =
1081
+ ["leptris parse recovered no document root " \
1082
+ "(empty or whitespace-only input)"]
1083
+ end
1084
+
1051
1085
  register_mint_klasses!(native_doc)
1052
1086
  ctx = _context || Context.new(:leptris)
1053
1087
  doc = Wrappers::Document.new(native_doc, ctx)
@@ -1191,7 +1225,7 @@ module Moxml
1191
1225
  ::Leptris::XML::ParseOptions.const_defined?(:SKIP_DUP_DETECTION) &&
1192
1226
  ::Leptris::XML::ParseOptions.const_defined?(:SKIP_SOURCE_POSITIONS)
1193
1227
 
1194
- def parse_flags(options, context: nil)
1228
+ def parse_flags(options, recover: false, context: nil)
1195
1229
  flags = 0
1196
1230
  flags |= ::Leptris::XML::ParseOptions::DTDATTR if options[:dtdattr] == true
1197
1231
  flags |= ::Leptris::XML::ParseOptions::KEEP_ENTITY_REFS if NATIVE_ENTITY_REFS &&
@@ -1203,6 +1237,13 @@ module Moxml
1203
1237
  flags |= po::SKIP_DUP_DETECTION if options[:skip_dup_detection] == true
1204
1238
  flags |= po::SKIP_SOURCE_POSITIONS if options[:skip_source_positions] == true
1205
1239
  end
1240
+ if recover
1241
+ # #369: flags and the recover kwarg ride the same
1242
+ # options struct (leptris-ruby's constructor composes
1243
+ # both).
1244
+ return ::Leptris::XML::ParseOptions.new(flags, recover: true)
1245
+ end
1246
+
1206
1247
  flags.zero? ? nil : ::Leptris::XML::ParseOptions.new(flags)
1207
1248
  end
1208
1249
 
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.129"
4
+ VERSION = "0.5.131"
5
5
  end
@@ -595,11 +595,24 @@ RSpec.describe Moxml::Adapter::Leptris do
595
595
  describe "recover-path parse diagnostics" do
596
596
  let(:ctx) { Moxml.new(:leptris) }
597
597
 
598
- it "records the fatal error on non-strict parses" do
599
- doc = ctx.parse("<root><unclosed>", strict: false)
600
- expect(doc.root).to be_nil
601
- expect(doc.parse_errors).not_to be_empty
602
- expect(doc.parse_errors).to all(be_a(String))
598
+ it "records the fatal error on non-strict parses below the recover floor" do
599
+ unless Moxml::Adapter::Leptris::RECOVER_MODE
600
+ doc = ctx.parse("<root><unclosed>", strict: false)
601
+ expect(doc.root).to be_nil
602
+ expect(doc.parse_errors).not_to be_empty
603
+ expect(doc.parse_errors).to all(be_a(String))
604
+ end
605
+ end
606
+
607
+ it "recovers an unclosed root at the floor (#369)" do
608
+ if Moxml::Adapter::Leptris::RECOVER_MODE
609
+ doc = ctx.parse("<root><unclosed>", strict: false)
610
+ expect(doc.root).not_to be_nil
611
+ # recovery diagnostics ride parse_errors once leptris-ruby
612
+ # #1200's DOM face lands; today the recovered tree is the
613
+ # contract.
614
+ expect(doc.parse_errors).to eq([])
615
+ end
603
616
  end
604
617
 
605
618
  it "answers [] on clean parses" do
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # #367: the bulk records stride carries the bound namespace URI
6
+ # for RESERVED-prefixed attributes on HTML-fragment-parsed trees —
7
+ # HTML trees carry no declarations, so the engine's attribute-URI
8
+ # face answers nil for xml:lang there; the bulk path mirrors the
9
+ # wrapper resolver's builtin table so records and wrapper agree.
10
+ RSpec.describe "materialize_fields reserved-prefix URIs (#367)" do
11
+ let(:ctx) { Moxml.new(:leptris) }
12
+ let(:frag) { ctx.parse_html_fragment('<span lang="en" xml:lang="en">text</span>') }
13
+ let(:span) { frag.children.first }
14
+
15
+ def records_attrs
16
+ out = nil
17
+ span.materialize_fields do |kind, _q, _p, _u, _ns, attrs, _t, _d| # rubocop:disable Metrics/ParameterLists,Lint/UnusedBlockArgument
18
+ out = attrs.dup if kind == :element
19
+ end
20
+ out
21
+ end
22
+
23
+ it "matches the wrapper API for prefixed attributes on fragment trees" do
24
+ records = records_attrs
25
+ expect(records).to eq(
26
+ ["lang", "en", nil, nil,
27
+ "lang", "en", "http://www.w3.org/XML/1998/namespace", "xml"],
28
+ )
29
+ end
30
+
31
+ it "agrees with the wrapper attribute API stride-for-stride" do
32
+ wrapper = span.attributes.map { |a| [a.name, a.value, a.namespace&.uri, a.namespace&.prefix] }
33
+ expect(records_attrs.each_slice(4).to_a).to eq(wrapper)
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # #369: non-strict parses RECOVER on engines carrying the mode
6
+ # (leptris 1.9.344+, leptris#1660) — the best-effort tree survives
7
+ # recoverable structural errors (libxml2 XML_PARSE_RECOVER
8
+ # semantics) instead of the rescue-to-empty fallback. Strict
9
+ # callers never recover: malformed input raises.
10
+ RSpec.describe "recover mode (#369)" do
11
+ let(:ctx) { Moxml.new(:leptris) }
12
+
13
+ describe "engines with the recover mode" do
14
+ before do
15
+ unless Moxml::Adapter::Leptris::RECOVER_MODE
16
+ skip "binding #{Leptris::VERSION} predates the recover mode"
17
+ end
18
+ end
19
+
20
+ it "recovers a mismatched closer into a tree" do
21
+ doc = ctx.parse("<root><a>x</b></root>", strict: false)
22
+ expect(doc.root).not_to be_nil
23
+ expect(doc.to_xml).to include("<a>x</a>")
24
+ end
25
+
26
+ it "closes unclosed elements at EOF" do
27
+ doc = ctx.parse("<root><a>x</root>", strict: false)
28
+ expect(doc.root).not_to be_nil
29
+ expect(doc.to_xml).to include("<a>x</a>")
30
+ end
31
+
32
+ it "keeps strict parsing raising" do
33
+ expect { ctx.parse("<root><a>x</b></root>", strict: true) }
34
+ .to raise_error(Moxml::ParseError)
35
+ end
36
+
37
+ it "raises by default (Context strict_parsing defaults true)" do
38
+ expect { ctx.parse("<root><a>x</b></root>") }
39
+ .to raise_error(Moxml::ParseError)
40
+ end
41
+
42
+ it "the recovered tree carries no parse errors (diagnostics ride #1200)" do
43
+ doc = ctx.parse("<root><a>x</b></root>", strict: false)
44
+ expect(doc.parse_errors).to eq([])
45
+ end
46
+ end
47
+
48
+ describe "engines below the recover floor" do
49
+ before do
50
+ if Moxml::Adapter::Leptris::RECOVER_MODE
51
+ skip "binding #{Leptris::VERSION} carries the recover mode"
52
+ end
53
+ end
54
+
55
+ it "keeps the empty-document fallback with diagnostics" do
56
+ doc = ctx.parse("<root><a>x</b></root>", strict: false)
57
+ expect(doc.root).to be_nil
58
+ expect(doc.parse_errors).not_to be_empty
59
+ end
60
+ end
61
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.129
4
+ version: 0.5.131
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -508,6 +508,7 @@ files:
508
508
  - spec/moxml/error_spec.rb
509
509
  - spec/moxml/html_fragment_spec.rb
510
510
  - spec/moxml/lazy_parse_spec.rb
511
+ - spec/moxml/materialize_namespace_uri_spec.rb
511
512
  - spec/moxml/materializer_spec.rb
512
513
  - spec/moxml/moxml_spec.rb
513
514
  - spec/moxml/namespace_spec.rb
@@ -530,6 +531,7 @@ files:
530
531
  - spec/moxml/plan_spec.rb
531
532
  - spec/moxml/processing_instruction_spec.rb
532
533
  - spec/moxml/readonly_parse_spec.rb
534
+ - spec/moxml/recover_mode_spec.rb
533
535
  - spec/moxml/sax/namespace_splitter_spec.rb
534
536
  - spec/moxml/sax_entity_parity_spec.rb
535
537
  - spec/moxml/sax_spec.rb