moxml 0.5.94 → 0.5.96
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/docs/_guides/advanced-features.adoc +18 -0
- data/lib/moxml/adapter/leptris.rb +16 -0
- data/lib/moxml/adapter/nokogiri.rb +1 -0
- data/lib/moxml/adapter/rexml.rb +6 -6
- data/lib/moxml/entity.rb +19 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_parse_skips_spec.rb +58 -0
- data/spec/moxml/c14n/character_encoding_spec.rb +57 -0
- data/spec/moxml/struct_plan_typed_spec.rb +4 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 011415dfeb5b083b1cb88c8a7b4619ed3cc896c51b78500fe849c02bbab055a4
|
|
4
|
+
data.tar.gz: a8b99c779efcca214047df53e1891b07961f0d3ab154e3f3538e83bfdbfb64f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1eb8163f7e725c42bba78d058abcece733204a414df60278d4fbfc477808160e8800eaa9fd5de5c011c1ce849d3f9e630eb667ebe15808cc6a614120a92fcd5
|
|
7
|
+
data.tar.gz: 5d4108fc8881b6c8073dd8de95c0023b3aaeca379e258fa5b8345a6c6016f0ee814b9d2c6b047ea1d91c724391e4fc8b6127090781c33665dcba59c536f8bb52
|
|
@@ -80,6 +80,24 @@ adapter = element.context.config.adapter
|
|
|
80
80
|
element = Moxml::Element.new(native_node, context)
|
|
81
81
|
----
|
|
82
82
|
|
|
83
|
+
=== Parse bookkeeping opt-outs (leptris)
|
|
84
|
+
|
|
85
|
+
Data-pipeline parses that never read diagnostics or source positions can
|
|
86
|
+
skip that bookkeeping. Both are opt-in per call; the default parse is
|
|
87
|
+
byte-identical either way.
|
|
88
|
+
|
|
89
|
+
[source,ruby]
|
|
90
|
+
----
|
|
91
|
+
# Duplicate attributes admit silently: queries answer first-wins, but
|
|
92
|
+
# the duplicate stays in the tree, so the serialized form can carry it.
|
|
93
|
+
doc = Moxml.new.parse(xml, skip_dup_detection: true)
|
|
94
|
+
|
|
95
|
+
# Source columns degrade; lines still resolve.
|
|
96
|
+
doc = Moxml.new.parse(xml, skip_source_positions: true)
|
|
97
|
+
----
|
|
98
|
+
|
|
99
|
+
Without the flags, a duplicate attribute records a recover diagnostic in
|
|
100
|
+
`doc.parse_diagnostics` and parses with the first value.
|
|
83
101
|
|
|
84
102
|
See also:
|
|
85
103
|
|
|
@@ -856,6 +856,17 @@ module Moxml
|
|
|
856
856
|
# Nokogiri semantics: blanks kept, no ATTLIST defaults).
|
|
857
857
|
# noblanks forwards only once the engine flag is libxml2-safe
|
|
858
858
|
# (see ENGINE_NOBLANKS_SAFE); otherwise it is moxml-side.
|
|
859
|
+
#
|
|
860
|
+
# The SKIP_ bookkeeping opt-outs (leptris-ruby#352, engine
|
|
861
|
+
# 1.9.272): duplicate attributes admit silently (first wins,
|
|
862
|
+
# no recover diagnostic) and source columns degrade (lines
|
|
863
|
+
# still resolve). Caller opt-in only — never defaults. The
|
|
864
|
+
# constant probe is the dependency-boundary check (the gem
|
|
865
|
+
# is zero-dep; user-resolved binding versions predate them).
|
|
866
|
+
NATIVE_PARSE_SKIPS =
|
|
867
|
+
::Leptris::XML::ParseOptions.const_defined?(:SKIP_DUP_DETECTION) &&
|
|
868
|
+
::Leptris::XML::ParseOptions.const_defined?(:SKIP_SOURCE_POSITIONS)
|
|
869
|
+
|
|
859
870
|
def parse_flags(options, context: nil)
|
|
860
871
|
flags = 0
|
|
861
872
|
flags |= ::Leptris::XML::ParseOptions::DTDATTR if options[:dtdattr] == true
|
|
@@ -863,6 +874,11 @@ module Moxml
|
|
|
863
874
|
(context&.config&.entity_mode == :keep ||
|
|
864
875
|
options[:keep_entity_refs] == true)
|
|
865
876
|
flags |= ::Leptris::XML::ParseOptions::NOBLANKS if options[:noblanks] == true && ENGINE_NOBLANKS_SAFE
|
|
877
|
+
if NATIVE_PARSE_SKIPS
|
|
878
|
+
po = ::Leptris::XML::ParseOptions
|
|
879
|
+
flags |= po::SKIP_DUP_DETECTION if options[:skip_dup_detection] == true
|
|
880
|
+
flags |= po::SKIP_SOURCE_POSITIONS if options[:skip_source_positions] == true
|
|
881
|
+
end
|
|
866
882
|
flags.zero? ? nil : ::Leptris::XML::ParseOptions.new(flags)
|
|
867
883
|
end
|
|
868
884
|
|
|
@@ -59,6 +59,7 @@ module Moxml
|
|
|
59
59
|
# (Document/C14n), which is circular while THIS adapter file
|
|
60
60
|
# is still loading.
|
|
61
61
|
def native_inclusive10(native)
|
|
62
|
+
return nil if native.nil?
|
|
62
63
|
return nil unless native_c14n_byte_safe?
|
|
63
64
|
|
|
64
65
|
native.canonicalize(::Nokogiri::XML::XML_C14N_1_0)
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -25,12 +25,12 @@ module Moxml
|
|
|
25
25
|
def parse(xml, options = {}, _context = nil)
|
|
26
26
|
xml = "" if xml.nil?
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
# Transcode from the input's own encoding tag; encode returns
|
|
29
|
+
# a fresh string (or self, already UTF-8) — the caller's
|
|
30
|
+
# buffer is never retagged in place. The old non-frozen
|
|
31
|
+
# branch force_encoding!-mutated the CALLER's string,
|
|
32
|
+
# corrupting it (invalid UTF-8 tag) for every later parse.
|
|
33
|
+
processed_xml = xml.encode("UTF-8")
|
|
34
34
|
|
|
35
35
|
# Preprocess entities to avoid double-escaping on output
|
|
36
36
|
processed_xml = preprocess_entities(processed_xml)
|
data/lib/moxml/entity.rb
CHANGED
|
@@ -112,6 +112,7 @@ module Moxml
|
|
|
112
112
|
else
|
|
113
113
|
xml.encode("UTF-8")
|
|
114
114
|
end
|
|
115
|
+
str = rewrite_declared_encoding(str)
|
|
115
116
|
# Fast path: no `&` means no entity references to mark — skip
|
|
116
117
|
# the regex scan and string allocation entirely. The vast
|
|
117
118
|
# majority of XML payloads contain no entity references.
|
|
@@ -139,6 +140,24 @@ module Moxml
|
|
|
139
140
|
preprocess_with_marker_flag(xml)[0]
|
|
140
141
|
end
|
|
141
142
|
|
|
143
|
+
# Engines honor the DECLARED encoding and re-decode the bytes they
|
|
144
|
+
# are handed — moxml always feeds UTF-8 (see the normalization
|
|
145
|
+
# above), so a declaration naming another encoding must be
|
|
146
|
+
# rewritten to match, or the already-transcoded bytes decode a
|
|
147
|
+
# second time into mojibake ("café" -> "café"). The anchored match
|
|
148
|
+
# fails on the first two bytes for documents without a declaration.
|
|
149
|
+
DECLARED_ENCODING_RE = /\A(<\?xml[^>]*?encoding=)['"][^'"]*['"]/
|
|
150
|
+
private_constant :DECLARED_ENCODING_RE
|
|
151
|
+
|
|
152
|
+
def rewrite_declared_encoding(str)
|
|
153
|
+
# The passthrough contract (Entity spec: plain text returns the
|
|
154
|
+
# SAME object) requires no allocation when no declaration names
|
|
155
|
+
# another encoding — both gates are allocation-free.
|
|
156
|
+
return str unless str.start_with?("<?xml") && str.include?("encoding=")
|
|
157
|
+
|
|
158
|
+
str.sub(DECLARED_ENCODING_RE) { "#{Regexp.last_match(1)}\"UTF-8\"" }
|
|
159
|
+
end
|
|
160
|
+
|
|
142
161
|
# Resolve numeric (&#NN; / &#xNN;) and the five standard named
|
|
143
162
|
# (& < > " ') XML entity references in one
|
|
144
163
|
# single pass. The resulting characters are data; no further
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
DUP_XML = %(<r><e id="1">text</e><e id="2" id="3"/></r>)
|
|
6
|
+
POS_XML = %(<r><e id="1">text</e></r>)
|
|
7
|
+
|
|
8
|
+
RSpec.describe "Moxml leptris parse bookkeeping opt-outs" do
|
|
9
|
+
# leptris-ruby#352 (binding 1.9.273.1, engine 1.9.272 Door A):
|
|
10
|
+
# SKIP_DUP_DETECTION admits duplicate attributes silently (first
|
|
11
|
+
# wins, no recover diagnostic); SKIP_SOURCE_POSITIONS degrades
|
|
12
|
+
# source columns (lines still resolve). Caller opt-in only.
|
|
13
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
14
|
+
let(:adapter) { ctx.config.adapter }
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
skip "binding without the SKIP_ parse flags" unless
|
|
18
|
+
Leptris::XML::ParseOptions.const_defined?(:SKIP_DUP_DETECTION)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "drops the duplicate-attribute diagnostic when requested" do
|
|
22
|
+
doc = ctx.parse(DUP_XML, skip_dup_detection: true)
|
|
23
|
+
expect(adapter.parse_diagnostics(doc.native)).to eq([])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "keeps the duplicate-attribute diagnostic by default" do
|
|
27
|
+
doc = ctx.parse(DUP_XML)
|
|
28
|
+
expect(adapter.parse_diagnostics(doc.native).map { |d| d[:kind] }).to include(:recover)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "first attribute wins either way" do
|
|
32
|
+
expect(ctx.parse(DUP_XML, skip_dup_detection: true).root.children[1][:id])
|
|
33
|
+
.to eq(ctx.parse(DUP_XML).root.children[1][:id])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "keeps lines but degrades columns for skipped positions" do
|
|
37
|
+
full = ctx.parse(POS_XML).root.children[0].source_position
|
|
38
|
+
skipped = ctx.parse(POS_XML, skip_source_positions: true)
|
|
39
|
+
.root.children[0].source_position
|
|
40
|
+
expect(skipped[:line]).to eq(full[:line])
|
|
41
|
+
expect(skipped[:col_start]).to be < full[:col_start]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "keeps the duplicate in the tree — queries still answer first-wins" do
|
|
45
|
+
# The engine admits the duplicate silently: reads see the first
|
|
46
|
+
# value, but the second attribute stays in the tree, so the
|
|
47
|
+
# serialized form can carry it. Callers opting in trade the
|
|
48
|
+
# well-formed-output guarantee for the bookkeeping speed.
|
|
49
|
+
fast = ctx.parse(DUP_XML, skip_dup_detection: true)
|
|
50
|
+
expect(fast.root.children[1][:id]).to eq("2")
|
|
51
|
+
expect(fast.to_xml).to include('id="2" id="3"')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "still raises on fatal errors with skips on" do
|
|
55
|
+
expect { ctx.parse("<r><e></r>", skip_dup_detection: true) }
|
|
56
|
+
.to raise_error(Moxml::ParseError)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# TODO.c14n/13: encoding edge cases. moxml normalizes every parse to
|
|
7
|
+
# UTF-8 bytes and rewrites the declaration to match — engines honor
|
|
8
|
+
# the DECLARED encoding and would otherwise re-decode the already
|
|
9
|
+
# transcoded bytes into mojibake.
|
|
10
|
+
CAF_E_UTF8 = [60, 114, 62, 99, 97, 102, 195, 169, 60, 47, 114, 62].freeze
|
|
11
|
+
LATIN1_XML = (+"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xE9</r>").force_encoding("ISO-8859-1")
|
|
12
|
+
BOM_XML = (+"\xEF\xBB\xBF<r/>").force_encoding("UTF-8")
|
|
13
|
+
NFC_XML = (+"<r>caf\xC3\xA9</r>").force_encoding("UTF-8")
|
|
14
|
+
|
|
15
|
+
# oga's parse layer leaks the declaration as a PI node and the BOM as
|
|
16
|
+
# text (pre-existing, documented in the W3C examples spec).
|
|
17
|
+
OGA_LEAK = %i[oga].freeze
|
|
18
|
+
|
|
19
|
+
RSpec.describe "Moxml character encoding at the parse boundary" do
|
|
20
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
21
|
+
context "with the #{adapter_name} adapter" do
|
|
22
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
23
|
+
|
|
24
|
+
unless OGA_LEAK.include?(adapter_name)
|
|
25
|
+
it "transcodes declared Latin-1 to UTF-8 bytes" do
|
|
26
|
+
doc = ctx.parse(LATIN1_XML.dup)
|
|
27
|
+
expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "strips a leading UTF-8 BOM" do
|
|
31
|
+
doc = ctx.parse(BOM_XML.dup)
|
|
32
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<r></r>")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "passes NFC content through without normalization" do
|
|
37
|
+
doc = ctx.parse(NFC_XML.dup)
|
|
38
|
+
expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "does not mutate the caller's input string" do
|
|
42
|
+
input = LATIN1_XML.dup
|
|
43
|
+
expect(input).not_to be_frozen
|
|
44
|
+
ctx.parse(input)
|
|
45
|
+
expect(input.encoding).to eq(Encoding::ISO_8859_1)
|
|
46
|
+
expect(input.valid_encoding?).to be(true)
|
|
47
|
+
expect(input.bytes).to eq(LATIN1_XML.bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "serializes transcoded documents as UTF-8 declarations" do
|
|
51
|
+
doc = ctx.parse(LATIN1_XML.dup)
|
|
52
|
+
expect(doc.to_xml).to include("UTF-8")
|
|
53
|
+
expect(doc.to_xml).not_to include("ISO-8859-1")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
+
# The per-item target class for the typed plan: Struct gives the
|
|
6
|
+
# accessors StructPlan assigns the cast slots to.
|
|
7
|
+
TypedItem = Struct.new(:price, :qty, :ok, :label)
|
|
8
|
+
|
|
5
9
|
# Typed plan scalars (leptris plan ABI, #1269a consumer face):
|
|
6
10
|
# [slot, type] declarations cast in C — Integer/Float/TrueClass/
|
|
7
11
|
# FalseClass members with no Ruby String materialized; unparseable
|
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.
|
|
4
|
+
version: 0.5.96
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -438,6 +438,7 @@ files:
|
|
|
438
438
|
- spec/moxml/adapter/entity_restoration_spec.rb
|
|
439
439
|
- spec/moxml/adapter/headed_ox_spec.rb
|
|
440
440
|
- spec/moxml/adapter/leptris_doc_parts_spec.rb
|
|
441
|
+
- spec/moxml/adapter/leptris_parse_skips_spec.rb
|
|
441
442
|
- spec/moxml/adapter/leptris_record_driver_spec.rb
|
|
442
443
|
- spec/moxml/adapter/leptris_sax_dup_attr_spec.rb
|
|
443
444
|
- spec/moxml/adapter/leptris_sax_records_spec.rb
|
|
@@ -460,6 +461,7 @@ files:
|
|
|
460
461
|
- spec/moxml/c14n/adapter_delegation_spec.rb
|
|
461
462
|
- spec/moxml/c14n/api_spec.rb
|
|
462
463
|
- spec/moxml/c14n/c14n_spec.rb
|
|
464
|
+
- spec/moxml/c14n/character_encoding_spec.rb
|
|
463
465
|
- spec/moxml/c14n/comments_pis_spec.rb
|
|
464
466
|
- spec/moxml/c14n/inclusive10_spec.rb
|
|
465
467
|
- spec/moxml/c14n/namespace_edge_cases_spec.rb
|