moxml 0.5.150 → 0.5.151

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: 73b67b633933c60095bdeafd744a8f7a827066edf478245e9c2f49aadf1d75e6
4
- data.tar.gz: 92320b13e8bb745cd5b650e85d4fa7b8397e082871a7c255cb61f637595c6048
3
+ metadata.gz: 2d27f78bba118e07aa9790336f2d7cbc0545e48217fcaf7ec93b222a170cc398
4
+ data.tar.gz: 6095220fc10e1f483d1ee3aed9b83c70ea1a61f0f304e00f4863a7fd52aca90c
5
5
  SHA512:
6
- metadata.gz: c34b4df4cf09797b5a28ec909e0df41c970897676154fa1b66dc9ebbd18cdccb13408b83f5647c44d41cfda7ebcbf09571314d0bfe71b572a58dde972e78994b
7
- data.tar.gz: d926c442a8b0dc0f9d159f27f42772a3af48358fbbd67d62d7cec1cdd51c609110def03466fa93c5b95b2eccfdaa7ab34e26c818b30b2651a996b9879c63d699
6
+ metadata.gz: 7fec02ca86ee8bbe73cd220363316147d35f94c010399c3b8163bc616460f0b03382c249befa262304e5182532c1e2abca6852707345b64edfab1c8c1d1661d9
7
+ data.tar.gz: 47f9e080e840c83a0b006d906bbf5e526d12324812c8d1a63c49cb12f2c3f0610078f98120a326080d6ba189bac7fe0131d82e628e58ad28878e1537bd1df96c
@@ -21,7 +21,7 @@ validation target, never a stand-in.
21
21
 
22
22
  | `lutaml/lutaml-model` | — | 6213+ examples green on the released pair
23
23
 
24
- | `lutaml/canon` | — | 2573 examples, 0 failures locally; in CI the Windows leg fails one JSON-engine parity example (`yaml_engine_parity_spec.rb:123`, the resolved json gem's duplicate-key verdict) — unrelated to XML processing, owner-held in canon
24
+ | `lutaml/canon` | — | 2573 examples, 0 failures (a transient Windows failure of one JSON-engine parity example — yeptris 0.6.34.2's undefined `MARSHAL_STRICT_DUP_KEYS` under json 3 — cleared with yeptris 0.6.34.3)
25
25
 
26
26
  | `metanorma/metanorma-standoc` | ghostscript | 627 examples green in CI on main (the boilerplate-fixture failure seen during the 04/05 validation was resolved by the 06 adapter fixes; a few local-only failures still need generated assets)
27
27
 
@@ -42,7 +42,7 @@ a declared, decided lane.
42
42
  |===
43
43
  | Consumer | Use | Disposition
44
44
 
45
- | canon | HTML5 parse-lint (`parse_html(collect_errors:)`) | **Facade capability shipped**: `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column. canon adopts it in canon#281, which is owner-held (moxml does not merge canon PRs).
45
+ | canon | HTML5 parse-lint (`parse_html(collect_errors:)`) | **Retired** (canon#281, merged): `Moxml::Context#parse_html` forwards `collect_errors` and `Document#parse_diagnostics` answers the §13.2.5 token stream with code/line/column; canon rides the facade.
46
46
 
47
47
  | canon | c14n native delegation (`doc.native.c14n`) | **Decided lane**: canon owns the c14n reference implementation and byte-probes the engine against its Ruby path; it is the backend behind moxml's own native delegation.
48
48
 
@@ -580,6 +580,13 @@ namespace_validation_mode: :strict)
580
580
  false
581
581
  end
582
582
 
583
+ # Whether the mint pre-writes the consumer's contract ivars
584
+ # in C (create_primed_element), letting Document#create_element
585
+ # skip its Ruby prime pass. Base: never.
586
+ def mints_primed?
587
+ false
588
+ end
589
+
583
590
  # Whether add_child can keep tracking the same native —
584
591
  # adapters that may recreate the node on attach (libxml's
585
592
  # doc.root=) override to false so the wrapper refresh path
@@ -1193,14 +1193,26 @@ module Moxml
1193
1193
  # entry are correct; upstream bug filed). Track the state per
1194
1194
  # binding document and route XPath to the reference Ruby
1195
1195
  # engine until a child insertion rebuilds the engine index.
1196
+ #
1197
+ # FIXED upstream in libleptris 1.9.378 (leptris/leptris#1802):
1198
+ # every splice path invalidates the index, so the guard is
1199
+ # dead weight on modern engines — and it sat on the builder
1200
+ # hot path (one Monitor-synchronized attachment set plus a
1201
+ # document hop per add_child). Retirement is probed, not
1202
+ # version-gated: any build that passes the #519 repro opts
1203
+ # itself out, no moxml release needed.
1196
1204
  XPATH_INDEX_STALE = :xpath_index_stale
1197
1205
 
1198
1206
  def mark_xpath_index_stale(node)
1207
+ return if native_xpath_index_fixed?
1208
+
1199
1209
  doc = xpath_index_document(node)
1200
1210
  attachments.set(doc, XPATH_INDEX_STALE, true) if doc
1201
1211
  end
1202
1212
 
1203
1213
  def clear_xpath_index_stale(node)
1214
+ return if native_xpath_index_fixed?
1215
+
1204
1216
  doc = xpath_index_document(node)
1205
1217
  attachments.set(doc, XPATH_INDEX_STALE, false) if doc
1206
1218
  end
@@ -1762,6 +1774,23 @@ module Moxml
1762
1774
  (owner_doc || create_document).create_element(name.to_s)
1763
1775
  end
1764
1776
 
1777
+ # MINT_PRIMED: the compiled ext's create_element takes
1778
+ # optional (context, adapter) and pre-writes the contract
1779
+ # ivars in C — arity -1 vs the old 2. Self-gating: no
1780
+ # version floor, the probe reads the loaded ext.
1781
+ MINT_PRIMED = defined?(::Leptris::XML::NativeNode) &&
1782
+ ::Leptris::XML::NativeNode
1783
+ .method(:create_element).arity == -1
1784
+
1785
+ def mints_primed?
1786
+ MINT_PRIMED
1787
+ end
1788
+
1789
+ def create_primed_element(name, owner_doc:, context:, adapter:)
1790
+ validate_element_name(name)
1791
+ owner_doc.native_create_element(name.to_s, context, adapter)
1792
+ end
1793
+
1765
1794
  def create_native_text(content, owner_doc = nil)
1766
1795
  if NATIVE_MUTATIONS_COHERENT && owner_doc
1767
1796
  return owner_doc.native_create_text(content)
@@ -2408,6 +2437,7 @@ module Moxml
2408
2437
  # callers fall back to create_element + set_attribute +
2409
2438
  # add_child when absent.
2410
2439
  def create_element_with_attrs(parent, name, attrs)
2440
+ validate_element_name(name)
2411
2441
  binding_parent = to_binding(parent)
2412
2442
  doc = binding_parent.document or return nil
2413
2443
  return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
@@ -3129,7 +3159,32 @@ module Moxml
3129
3159
  end.join
3130
3160
  end
3131
3161
 
3162
+ # SET_TEXT_NATIVE: bindings with the unshadowed C alias
3163
+ # (the add_child_native pattern — set_text_native aliases
3164
+ # content= on the TypedData self) take the one-frame write:
3165
+ # no doc_for climb, no address read, no module dispatch.
3166
+ # Self-gating by respond_to? — older exts fall through to
3167
+ # the set_binding_text face.
3168
+ SET_TEXT_NATIVE = defined?(::Leptris::XML::NativeNode) &&
3169
+ ::Leptris::XML::NativeNode.method_defined?(:set_text_native)
3170
+
3132
3171
  def set_text_content(node, content)
3172
+ if SET_TEXT_NATIVE && node.is_a?(::Leptris::XML::NativeNode)
3173
+ begin
3174
+ node.set_text_native(content.to_s)
3175
+ return
3176
+ rescue ::Leptris::XML::Error => e
3177
+ # Same #401 contract as the fallback below: a detached
3178
+ # (parentless) node re-raises as the named Moxml::Error.
3179
+ raise if node.parent
3180
+
3181
+ raise Moxml::Error,
3182
+ "cannot set text content on a detached " \
3183
+ "(parentless) node — attach it to a document " \
3184
+ "first (issue #401): #{e.message}"
3185
+ end
3186
+ end
3187
+
3133
3188
  # Native-layer fast path (leptris-ruby#460): the C setter
3134
3189
  # takes document+address directly — no binding twin mint
3135
3190
  # per write. The Identity wrapper's own content= would
@@ -109,6 +109,10 @@ module Moxml
109
109
  # over-invalidate (global bump instead of local) — a no-op
110
110
  # for correctness.
111
111
  if name.start_with?("xmlns")
112
+ # Introducing (or rewriting) a namespace declaration: mark the
113
+ # context BEFORE the invalidation, so this write's generation
114
+ # bump fans out to every wrapper's memoized scope reads.
115
+ element.context.mark_namespaces_active!
112
116
  adapter.set_attribute(element.native, name, value)
113
117
  element.invalidate_attribute_cache!
114
118
  return value
data/lib/moxml/context.rb CHANGED
@@ -90,7 +90,10 @@ module Moxml
90
90
  end
91
91
 
92
92
  def bump_children_generation
93
- @children_generation = children_generation + 1
93
+ # One dispatch, not two: children_generation re-dispatched just
94
+ # to default nil. Shared counter — node wrappers' children memos
95
+ # key on it.
96
+ @children_generation = @children_generation.to_i + 1
94
97
  end
95
98
 
96
99
  def namespace_scope_generation
@@ -101,6 +104,22 @@ module Moxml
101
104
  @namespace_scope_generation += 1
102
105
  end
103
106
 
107
+ # Namespace scopes only need the per-mutation generation bump
108
+ # while namespaces can exist. A builder-only document (no parse,
109
+ # no xmlns write) never has one, and the bump sat on the attach
110
+ # hot path (deletion experiment: build row 2.128x -> 1.923x).
111
+ # Every namespace introduction route marks the context active
112
+ # first — parse faces and the xmlns attribute-write face — so
113
+ # the sticky flag never misses an introduction.
114
+ def namespace_scopes_active?
115
+ @namespace_scopes_active = false if @namespace_scopes_active.nil?
116
+ @namespace_scopes_active
117
+ end
118
+
119
+ def mark_namespaces_active!
120
+ @namespace_scopes_active = true
121
+ end
122
+
104
123
  def entity_registry
105
124
  @entity_registry ||= build_entity_registry
106
125
  end
@@ -128,6 +147,7 @@ module Moxml
128
147
  # Parse with adapter, passing self (context) so adapter can use our config
129
148
  parsed_options = default_options.merge(options)
130
149
  doc = config.adapter.parse(xml_string, parsed_options, self)
150
+ mark_namespaces_active!
131
151
 
132
152
  # Set declaration flag on Document wrapper (proper OOP)
133
153
  doc.has_xml_declaration = has_declaration if doc.is_a?(Document)
@@ -233,7 +253,9 @@ module Moxml
233
253
  # are the result either way.
234
254
  def parse_fragment(xml)
235
255
  adapter = config.adapter
236
- adapter.parse_fragment(xml, self).map do |node|
256
+ nodes = adapter.parse_fragment(xml, self)
257
+ mark_namespaces_active!
258
+ nodes.map do |node|
237
259
  # Adapters that tie a parent_node lifetime chain (#245)
238
260
  # return wrapped nodes — pass them through untouched;
239
261
  # re-wrapping would race the weak wrapper map.
@@ -242,6 +264,7 @@ module Moxml
242
264
  end
243
265
 
244
266
  def iterparse(xml, mode: :top_level, &block)
267
+ mark_namespaces_active!
245
268
  config.adapter.iterparse(xml, mode, self, &block)
246
269
  end
247
270
 
@@ -163,7 +163,17 @@ module Moxml
163
163
  end
164
164
 
165
165
  def create_element(name)
166
- native = adapter.create_element(name, owner_doc: @native)
166
+ # The preprimed mint (leptris binding with the C-primed
167
+ # create_element): the mint already wrote @context/@adapter in
168
+ # C, so the returned node IS the finished wrapper and the Ruby
169
+ # prime pass disappears from the build path.
170
+ native = if adapter.mints_primed?
171
+ adapter.create_primed_element(name, owner_doc: @native,
172
+ context: context,
173
+ adapter: adapter)
174
+ else
175
+ adapter.create_element(name, owner_doc: @native)
176
+ end
167
177
  # leptris-ruby#508 mints registered klasses at create: the node
168
178
  # IS a contract-carrying identity (@native is self) — prime it
169
179
  # in place instead of shelling a second wrapper (a raw mint
data/lib/moxml/element.rb CHANGED
@@ -276,6 +276,7 @@ module Moxml
276
276
  def add_namespace(prefix, uri)
277
277
  adapter.create_namespace(@native, prefix, uri,
278
278
  namespace_validation_mode: context.config.namespace_validation_mode)
279
+ context.mark_namespaces_active!
279
280
  invalidate_namespace_cache!
280
281
  self
281
282
  rescue ValidationError => e
@@ -530,8 +531,19 @@ module Moxml
530
531
 
531
532
  # Clear the namespace caches and bump the context's scope
532
533
  # generation so every wrapper — including ones not reachable from
533
- # any children cache — recomputes on next read.
534
+ # any children cache — recomputes on next read. While namespaces
535
+ # cannot exist (context.namespace_scopes_active? is false) the
536
+ # memos can only hold namespace-free reads, so skipping the whole
537
+ # body is exact — and the nil-assignment form would MATERIALIZE
538
+ # three unset ivars per attached node, which is the cost this
539
+ # gate exists to remove. Every introduction route — parse faces
540
+ # and the xmlns write face — marks the context BEFORE its own
541
+ # invalidation, so the introducing mutation still clears and
542
+ # bumps; in-scope reads are generation-keyed, so other wrappers
543
+ # recompute on the bump.
534
544
  def invalidate_namespace_cache!
545
+ return unless context.namespace_scopes_active?
546
+
535
547
  @namespaces = nil
536
548
  @namespace_definitions = nil
537
549
  @in_scope_namespaces = nil
data/lib/moxml/node.rb CHANGED
@@ -30,9 +30,11 @@ module Moxml
30
30
  def prime_contract(native, context, adapter = nil, node_type = nil)
31
31
  @context = context
32
32
  @native = native
33
- @parent_node = nil
34
- @adapter = adapter
35
- @node_type_cached = node_type
33
+ # @parent_node and @node_type_cached stay UNSET when empty —
34
+ # every reader is nil-tolerant, and a nil write materializes the
35
+ # ivar (object-shape transition) on every created node.
36
+ @adapter = adapter unless adapter.nil?
37
+ @node_type_cached = node_type unless node_type.nil?
36
38
  self
37
39
  end
38
40
 
@@ -214,10 +216,16 @@ module Moxml
214
216
  def attach_child(node)
215
217
  context.bump_children_generation
216
218
  node = prepare_node(node)
217
- adapter.add_child(@native, node.native)
219
+ oc = node.context
220
+ if !oc.nil? && !oc.equal?(context) &&
221
+ oc.namespace_scopes_active?
222
+ context.mark_namespaces_active!
223
+ end
224
+ a = adapter
225
+ a.add_child(@native, node.native)
218
226
  # Refresh native in case adapter changed identity (e.g., LibXML
219
227
  # doc.root=); stable-identity adapters skip the round trip.
220
- unless adapter.native_identity_stable?
228
+ unless a.native_identity_stable?
221
229
  refreshed = adapter.actual_native(node.native, @native)
222
230
  node.refresh_native!(refreshed) if refreshed && refreshed != node.native
223
231
  end
@@ -231,6 +239,7 @@ module Moxml
231
239
  def attach_previous_sibling(node)
232
240
  context.bump_children_generation
233
241
  node = prepare_node(node)
242
+ absorb_namespace_activity(node)
234
243
  inserted = adapter.add_previous_sibling(@native, node.native)
235
244
  refresh_inserted_native(node, inserted)
236
245
  # Invalidate the parent's memoized children list. The wrapper-side
@@ -245,6 +254,7 @@ module Moxml
245
254
  def attach_next_sibling_to(anchor, node)
246
255
  context.bump_children_generation
247
256
  node = prepare_node(node)
257
+ absorb_namespace_activity(node)
248
258
  inserted = adapter.add_next_sibling(anchor.native, node.native)
249
259
  refresh_inserted_native(node, inserted)
250
260
  anchor.parent&.invalidate_children_cache!
@@ -252,6 +262,16 @@ module Moxml
252
262
  self
253
263
  end
254
264
 
265
+ # A node adopted from ANOTHER context can carry namespaces this
266
+ # context's own faces (parse, xmlns writes) never saw; import the
267
+ # source context's activity flag so the gated scope-generation
268
+ # bump stays armed here too.
269
+ def absorb_namespace_activity(node)
270
+ oc = node.context
271
+ return if oc.nil? || oc.equal?(context)
272
+ context.mark_namespaces_active! if oc.namespace_scopes_active?
273
+ end
274
+
255
275
  def refresh_inserted_native(node, inserted)
256
276
  return unless adapter.add_sibling_returns_inserted?
257
277
  return if inserted.nil? || inserted.equal?(node.native)
@@ -773,7 +793,10 @@ module Moxml
773
793
  protected
774
794
 
775
795
  def invalidate_children_cache!
776
- @children = nil
796
+ # Only nil an EXISTING memo: nil-assignment materializes an
797
+ # unset ivar (object-shape transition), and the builder hot
798
+ # path invalidates parents whose children memo was never read.
799
+ @children = nil unless @children.nil?
777
800
  end
778
801
 
779
802
  # Invalidate parent's cached children when this node
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.150"
4
+ VERSION = "0.5.151"
5
5
  end
@@ -865,5 +865,95 @@ RSpec.describe Moxml::Adapter::Leptris do
865
865
  expect(native).not_to be_nil
866
866
  expect(native.to_a.map { |n| n.name.to_s }).to eq(%w[a c b])
867
867
  end
868
+
869
+ it "never arms the guard on engines that invalidate on splice" do
870
+ skip "requires an engine whose node-set index survives sibling insertion (leptris-ruby#519)" unless described_class.native_xpath_index_fixed?
871
+
872
+ doc = ctx.parse("<root><anchor/></root>")
873
+ doc.xpath("//ext") # prime the engine index
874
+ doc.root.add_child(doc.create_element("ext"))
875
+ expect(doc.xpath("//ext").size).to eq(1)
876
+
877
+ expect(
878
+ described_class.attachments.get(
879
+ described_class.xpath_index_document(doc.root.native),
880
+ described_class.singleton_class.const_get(:XPATH_INDEX_STALE),
881
+ ),
882
+ ).to be_nil
883
+ end
884
+ end
885
+
886
+ describe "namespace scope-generation gate" do
887
+ let(:ctx) { Moxml.new(:leptris) }
888
+
889
+ # The per-attach scope-generation bump only matters once
890
+ # namespaces exist; the builder hot path must not pay it.
891
+ it "does not bump the scope generation while namespaces are absent" do
892
+ doc = ctx.create_document
893
+ root = doc.create_element("r")
894
+ doc.root = root
895
+ gen = ctx.namespace_scope_generation
896
+ root.add_child(doc.create_element("a"))
897
+ root.add_child(doc.create_element("b"))
898
+ expect(ctx.namespace_scope_generation).to eq(gen)
899
+ expect(ctx.namespace_scopes_active?).to be(false)
900
+ end
901
+
902
+ it "arms the gate when a write introduces a namespace" do
903
+ doc = ctx.create_document
904
+ root = doc.create_element("r")
905
+ doc.root = root
906
+ root["xmlns:foo"] = "urn:x"
907
+ expect(ctx.namespace_scopes_active?).to be(true)
908
+ expect(root.namespaces.map { |n| [n.prefix, n.uri] })
909
+ .to include(%w[foo urn:x])
910
+ end
911
+
912
+ it "is armed by parsed documents" do
913
+ doc = ctx.parse("<r xmlns:foo='urn:x'><foo:a/></r>")
914
+ expect(ctx.namespace_scopes_active?).to be(true)
915
+ expect(doc.xpath("//foo:a", "foo" => "urn:x").size).to eq(1)
916
+ end
917
+
918
+ it "imports the flag from a foreign context on adoption" do
919
+ donor_ctx = Moxml.new(:leptris)
920
+ donor = donor_ctx.parse("<x xmlns:foo='urn:x'/>")
921
+ doc = ctx.create_document
922
+ root = doc.create_element("r")
923
+ doc.root = root
924
+ expect(ctx.namespace_scopes_active?).to be(false)
925
+ root.add_child(donor.root)
926
+ expect(ctx.namespace_scopes_active?).to be(true)
927
+ expect(root.children.first.namespaces.map(&:uri))
928
+ .to include("urn:x")
929
+ end
930
+ end
931
+
932
+ # The README error-handling contract: an invalid element name must
933
+ # raise Moxml::ValidationError at CREATE time. The adapter's native
934
+ # fast paths (create_primed_element, create_element_with_attrs) mint
935
+ # from a name without passing through Base#create_element, where
936
+ # validate_element_name lives — the failure then surfaced late, at
937
+ # add_child, as Moxml::Error "Document already has a root element".
938
+ describe "element name validation on the create seam" do
939
+ let(:ctx) { Moxml.new(:leptris) }
940
+
941
+ it "raises ValidationError when create_element mints an invalid name" do
942
+ doc = ctx.parse("<root/>")
943
+ expect { doc.create_element("wrong,name") }
944
+ .to raise_error(Moxml::ValidationError, /Invalid XML element name/)
945
+ end
946
+
947
+ it "raises ValidationError when add_element mints an invalid name" do
948
+ doc = ctx.create_document
949
+ expect { doc.add_element("bad,name", id: "1") }
950
+ .to raise_error(Moxml::ValidationError, /Invalid XML element name/)
951
+ end
952
+
953
+ it "still mints valid names with punctuation allowed in XML Names" do
954
+ doc = ctx.parse("<root/>")
955
+ element = doc.create_element("ns:my-element.v2")
956
+ expect(element).to be_a(Moxml::Element)
957
+ end
868
958
  end
869
959
  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.150
4
+ version: 0.5.151
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.