moxml 0.5.145 → 0.5.146

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: 042f6cd21672a36e869b19c158819a3624d3a79a53de1fd083e83a377b577442
4
- data.tar.gz: 7cacbfbd44bb9a87aa252a638b8ac9d25fb5cb67b6e3845c8fdf9c5cf943df24
3
+ metadata.gz: b529948a14e67202c6ad6fc9acbe6c06c033ba022f93ada6bc9e4001554ea583
4
+ data.tar.gz: 68a76e1a69397aef0d0356e3ddd7bc200737a41333a70d6688cfca34d44e4c27
5
5
  SHA512:
6
- metadata.gz: 507216b5acdf26ae9cd5b4d69c12d2519b949fbe2017c39a804954a04659780c5432b4fe8b11262642ca1699e09eb453af08e230a653a9b530f55404b6d6f5f1
7
- data.tar.gz: 5d19c7d3d7e1ad18864d13b0b3eaf69f481b5f1a0c68fd652f29cd9bff4e7b9f95f4adf57698d77849371bc635f9ba3ef36c945f12a1b63e2a350e50fc886bbb
6
+ metadata.gz: 3775a27419325f8c764d6d686c547e88f75bdc073cbe354bdd97d2e0516e68a4f83896d4cd840c25be26f7d6aafdca24a596d3c6aa2893e8596ed95d646c16fa
7
+ data.tar.gz: 84c2f0abec2a55d990cdc921f9abac0c9206def8190d609fd298256dd035d586334dedc04bb97af496a98fe9053b2fa3b65f716a11e22e94516f2f8b91837e1b
@@ -463,25 +463,72 @@ module Moxml
463
463
  # instances). The hot bare reads super into the C methods
464
464
  # (exact-name = the bare-name contract).
465
465
  module Identity
466
+ # The C faces are captured before the contract modules
467
+ # shadow them — alias copies bind at this point, so the
468
+ # overrides below plain-dispatch the native reads
469
+ # (leptris-ruby#463 completed the surface: every contract
470
+ # face is a C method at 50-84ns).
466
471
  ELEMENT = Class.new(::Leptris::XML::NativeNode) do
472
+ alias_method :native_name, :name
467
473
  include ::Moxml::Node
468
474
  include ::Moxml::Element
475
+
476
+ # Memo form kept: a warm read is an ivar hit (~30ns
477
+ # vs the C call's 84ns); the cold read drops the
478
+ # adapter round-trip and its PI-check bind_call.
479
+ def name
480
+ @name ||= native_name
481
+ end
469
482
  end
470
483
  TEXT = Class.new(::Leptris::XML::NativeNode) do
484
+ alias_method :native_content, :content
471
485
  include ::Moxml::Node
472
486
  include ::Moxml::Text
487
+
488
+ # Text#content's memo + entity restore are the
489
+ # contract; the backing read drops the adapter
490
+ # round-trip (dispatch + is_a? + bind_call) for the
491
+ # C face.
492
+ def raw_content
493
+ native_content
494
+ end
473
495
  end
474
496
  CDATA = Class.new(::Leptris::XML::NativeNode) do
497
+ alias_method :native_content, :content
475
498
  include ::Moxml::Node
476
499
  include ::Moxml::Cdata
500
+
501
+ def content
502
+ native_content
503
+ end
477
504
  end
478
505
  COMMENT = Class.new(::Leptris::XML::NativeNode) do
506
+ alias_method :native_content, :content
479
507
  include ::Moxml::Node
480
508
  include ::Moxml::Comment
509
+
510
+ # The nil bridge stays for floor-admitted bindings
511
+ # where the native face answers nil for comment-kind
512
+ # (leptris-ruby#313).
513
+ def content
514
+ content = native_content
515
+ return ::Moxml::Adapter::Leptris.to_binding(self).content if content.nil?
516
+
517
+ content
518
+ end
481
519
  end
482
520
  PROCESSING_INSTRUCTION = Class.new(::Leptris::XML::NativeNode) do
521
+ alias_method :native_content, :content
483
522
  include ::Moxml::Node
484
523
  include ::Moxml::ProcessingInstruction
524
+
525
+ # Same nil bridge as Comment (leptris-ruby#344).
526
+ def content
527
+ content = native_content
528
+ return ::Moxml::Adapter::Leptris.to_binding(self).content if content.nil?
529
+
530
+ content
531
+ end
485
532
  end
486
533
 
487
534
  TYPES = {
@@ -537,6 +584,46 @@ module Moxml
537
584
  end
538
585
  ELEMENT.include(Reads)
539
586
  TEXT.include(Reads)
587
+
588
+ # leptris-ruby#503: name_read joins attr_read/text_read as
589
+ # the unshadowed contract faces. With them, the hot reads
590
+ # plain-dispatch the C faces (one Ruby frame over the
591
+ # ~45ns native call) instead of the adapter bind_call
592
+ # crossing. Face-probed — older bindings keep the
593
+ # adapter paths. The memos ride the existing
594
+ # invalidation: name= and clear_native_memo! clear
595
+ # @name, content= clears @content.
596
+ if NN.method_defined?(:name_read)
597
+ module ContractReads
598
+ def name
599
+ @name ||= name_read
600
+ end
601
+ end
602
+ ELEMENT.include(ContractReads)
603
+ end
604
+
605
+ if NN.method_defined?(:text_read)
606
+ module TextContractReads
607
+ # The stamp fast path is inlined (same ivar protocol
608
+ # as Reads#doc_entity_bearing?, which stays the
609
+ # single writer on recompute) — the two frames it
610
+ # saves are ~20% of the cold read.
611
+ def content
612
+ value = @content
613
+ return value if value
614
+
615
+ value = text_read
616
+ stamp = @doc_entity_stamp
617
+ if stamp && (stamp >> 1) == ::Moxml::Adapter::Leptris.serialize_generation
618
+ value = ::Moxml::Adapter::Leptris.restore_entities(value) if stamp.allbits?(1)
619
+ elsif doc_entity_bearing?
620
+ value = ::Moxml::Adapter::Leptris.restore_entities(value)
621
+ end
622
+ @content = value
623
+ end
624
+ end
625
+ TEXT.include(TextContractReads)
626
+ end
540
627
  end
541
628
 
542
629
  # leptris-ruby#246 klass-propagating reads: the read
@@ -2842,6 +2929,7 @@ module Moxml
2842
2929
  PARENT_AXIS_CACHE = XPath::Cache.new(100)
2843
2930
 
2844
2931
  def xpath(node, expression, namespaces = {})
2932
+ namespaces = normalize_xpath_namespaces(namespaces)
2845
2933
  node = to_binding(node) if NATIVE_READ_LAYER
2846
2934
  native = native_xpath(node, expression, namespaces)
2847
2935
  return native unless native.nil?
@@ -2873,6 +2961,7 @@ module Moxml
2873
2961
  end
2874
2962
 
2875
2963
  def at_xpath(node, expression, namespaces = {})
2964
+ namespaces = normalize_xpath_namespaces(namespaces)
2876
2965
  native = native_xpath(node, expression, namespaces, first_only: true)
2877
2966
  return native unless native.nil?
2878
2967
 
@@ -2880,6 +2969,16 @@ module Moxml
2880
2969
  result.is_a?(Array) ? result.first : result
2881
2970
  end
2882
2971
 
2972
+ # Bindings arrive in both spellings (symbol keys are the
2973
+ # common Nokogiri idiom); every seam below — the "xmlns"
2974
+ # reserved-key check and the native pairs set — is
2975
+ # string-keyed (issue #395).
2976
+ def normalize_xpath_namespaces(namespaces)
2977
+ return namespaces unless namespaces.is_a?(Hash)
2978
+
2979
+ namespaces.transform_keys(&:to_s)
2980
+ end
2981
+
2883
2982
  # @return [Array, Object, nil] native results, or nil when the
2884
2983
  # query must run on the Ruby engine
2885
2984
  def native_xpath(node, expression, namespaces, first_only: false)
@@ -607,6 +607,11 @@ module Moxml
607
607
  # XPath.match for every expression: get_elements only
608
608
  # handles simple child-name paths — attribute terminals,
609
609
  # absolute paths and predicates silently degrade there.
610
+ # REXML resolves prefixes by string spelling — symbol-keyed
611
+ # bindings (the common Nokogiri idiom) must normalize or
612
+ # namespaced elements never match (issue #395).
613
+ namespaces = namespaces.transform_keys(&:to_s) if
614
+ namespaces.is_a?(Hash)
610
615
  if namespaces && !namespaces.empty?
611
616
  ::REXML::XPath.match(node, expression, namespaces).to_a
612
617
  else
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.145"
4
+ VERSION = "0.5.146"
5
5
  end
@@ -40,6 +40,7 @@ module Moxml
40
40
  # @param namespaces [Hash, nil] Optional namespace prefix mappings
41
41
  # @return [Proc] Compiled Proc that accepts a document
42
42
  def self.compile_with_cache(ast, namespaces: nil)
43
+ namespaces = normalize_namespaces(namespaces)
43
44
  cache_key = namespaces ? [ast, namespaces] : ast
44
45
  CACHE.get_or_set(cache_key) { new(namespaces: namespaces).compile(ast) }
45
46
  end
@@ -48,12 +49,24 @@ module Moxml
48
49
  #
49
50
  # @param namespaces [Hash, nil] Optional namespace prefix mappings
50
51
  def initialize(namespaces: nil)
51
- @namespaces = namespaces
52
+ @namespaces = self.class.normalize_namespaces(namespaces)
52
53
  @literal_id = 0
53
54
  @predicate_nodesets = []
54
55
  @predicate_indexes = []
55
56
  end
56
57
 
58
+ # Prefix lookups are string-keyed (the parser yields prefix
59
+ # strings); symbol-keyed bindings — the common Nokogiri spelling
60
+ # (`xpath("//cml:molecule", cml: URI)`) — must resolve too, or
61
+ # the miss falls through to prefix-spelling matching and
62
+ # namespaced elements never match (issue #395).
63
+ def self.normalize_namespaces(namespaces)
64
+ return nil if namespaces.nil?
65
+ return namespaces unless namespaces.is_a?(Hash)
66
+
67
+ namespaces.transform_keys(&:to_s)
68
+ end
69
+
57
70
  # Compiles an XPath AST into a Ruby Proc
58
71
  #
59
72
  # @param ast [AST::Node] XPath AST to compile
@@ -11,11 +11,22 @@ require "spec_helper"
11
11
  # - Attribute-terminal steps work in every path position
12
12
  # (leptris-ruby#458's table: //a/@id, //@id, r/a/@id,
13
13
  # //*[f()]/@id — fixed by the engine's VM path work).
14
+ CML_NS = "http://www.xml-cml.org/schema"
15
+ ACI_NS = "http://www.xml-cml.org/schema/asciichem"
16
+
14
17
  RSpec.describe "XPath idioms across adapters" do
15
18
  let(:default_ns_xml) do
16
19
  '<svgmap xmlns="http://www.example.org/doc"><target href="a"/></svgmap>'
17
20
  end
18
21
  let(:plain_xml) { '<r><a id="x"><b/></a></r>' }
22
+ let(:cml_xml) do
23
+ <<~XML
24
+ <molecule xmlns="#{CML_NS}" xmlns:aci="#{ACI_NS}" id="m1">
25
+ <atomArray><atom id="a1"/></atomArray>
26
+ <aci:group multiplicity="2" bracket="paren" atomRefs="a1"/>
27
+ </molecule>
28
+ XML
29
+ end
19
30
 
20
31
  Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
21
32
  next if adapter_name == :headed_ox # same engine as :ox
@@ -56,6 +67,26 @@ RSpec.describe "XPath idioms across adapters" do
56
67
  expect(doc.xpath("//*[local-name()='a']/@id").first&.value).to eq("x")
57
68
  end
58
69
 
70
+ # Issue #395: bindings hashes arrive in both key spellings
71
+ # (symbols are the common Nokogiri idiom); the engines must
72
+ # resolve both — a symbol-key miss falls through to
73
+ # prefix-spelling matching and namespaced elements never match.
74
+ # asciichem's CML group extraction rides exactly this shape.
75
+ it "resolves symbol-keyed namespace bindings" do
76
+ doc = ctx.parse(cml_xml)
77
+ mols = doc.xpath("//cml:molecule", cml: CML_NS)
78
+ expect(mols.to_a.size).to eq(1)
79
+
80
+ groups = mols.first.xpath("./aci:group", aci: ACI_NS)
81
+ expect(groups.to_a.size).to eq(1)
82
+ expect(groups.first["multiplicity"]).to eq("2")
83
+ end
84
+
85
+ it "resolves string-keyed namespace bindings identically" do
86
+ doc = ctx.parse(cml_xml)
87
+ expect(doc.xpath("//cml:molecule", "cml" => CML_NS).to_a.size).to eq(1)
88
+ end
89
+
59
90
  # moxml#393: a relative name-test path on the DOCUMENT resolves
60
91
  # laxly, descendant-anchored at the root — Nokogiri's own
61
92
  # Document#at/#search semantics, now pinned for every adapter.
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # leptris-ruby#460 — the Identity contract reads ride the C faces
6
+ # by plain dispatch (NativeNode subclasses inherit the C-defined
7
+ # name/content since binding 1.9.359). The wrapper memo stays; the
8
+ # ADAPTER CROSSING (bind_call through node_name/text_content) goes.
9
+ RSpec.describe "leptris Identity contract reads" do
10
+ let(:ctx) { Moxml.new(:leptris) }
11
+
12
+ before do
13
+ unless Moxml::Adapter::Leptris::NATIVE_READ_LAYER &&
14
+ Leptris::XML::NativeNode.method_defined?(:name) &&
15
+ Leptris::XML::NativeNode.method_defined?(:content)
16
+ skip "binding #{defined?(Leptris) && Leptris::VERSION} lacks the C contract faces"
17
+ end
18
+ end
19
+
20
+ def bust(node, *ivars)
21
+ ivars.each do |iv|
22
+ node.remove_instance_variable(iv) if node.instance_variable_defined?(iv)
23
+ end
24
+ end
25
+
26
+ def forbid_crossing!(method)
27
+ singleton = Moxml::Adapter::Leptris.singleton_class
28
+ singleton.send(:alias_method, :"#{method}_without_460", method)
29
+ singleton.send(:define_method, method) { |*| raise "adapter #{method} crossing" }
30
+ method
31
+ end
32
+
33
+ def restore_crossing!(method)
34
+ singleton = Moxml::Adapter::Leptris.singleton_class
35
+ singleton.send(:alias_method, method, :"#{method}_without_460")
36
+ singleton.send(:remove_method, :"#{method}_without_460")
37
+ end
38
+
39
+ describe "element#name" do
40
+ it "answers without the adapter node_name crossing" do
41
+ unless Leptris::XML::NativeNode.method_defined?(:name_read)
42
+ skip "binding #{Leptris::VERSION} has no name_read face (adapter path is correct there)"
43
+ end
44
+ doc = ctx.parse('<root><item id="1">v</item></root>')
45
+ el = doc.root.children.first
46
+ expect(el.native).to be_a(Leptris::XML::NativeNode)
47
+
48
+ bust(el, :@name)
49
+ forbid_crossing!(:node_name)
50
+ begin
51
+ expect(el.name).to eq("item")
52
+ ensure
53
+ restore_crossing!(:node_name)
54
+ end
55
+ end
56
+
57
+ it "answers the local name for prefixed elements (cross-adapter contract)" do
58
+ doc = ctx.parse('<root xmlns:ns="urn:u"><ns:p/></root>')
59
+ el = doc.root.children.first
60
+ bust(el, :@name)
61
+ expect(el.name).to eq("p")
62
+ end
63
+
64
+ it "memoizes: warm reads return the same String" do
65
+ doc = ctx.parse("<root><item/></root>")
66
+ el = doc.root.children.first
67
+ first = el.name
68
+ expect(el.name).to equal(first)
69
+ end
70
+
71
+ it "re-reads after name=" do
72
+ doc = ctx.parse("<root><item/></root>")
73
+ el = doc.root.children.first
74
+ el.name = "renamed"
75
+ expect(el.name).to eq("renamed")
76
+ end
77
+ end
78
+
79
+ describe "text#content" do
80
+ it "answers without the adapter text_content crossing" do
81
+ unless Leptris::XML::NativeNode.method_defined?(:text_read)
82
+ skip "binding #{Leptris::VERSION} has no text_read face (adapter path is correct there)"
83
+ end
84
+ doc = ctx.parse("<root><item>v</item></root>")
85
+ tx = doc.root.children.first.children.first
86
+ expect(tx.native).to be_a(Leptris::XML::NativeNode)
87
+
88
+ bust(tx, :@content)
89
+ forbid_crossing!(:text_content)
90
+ begin
91
+ expect(tx.content).to eq("v")
92
+ ensure
93
+ restore_crossing!(:text_content)
94
+ end
95
+ end
96
+
97
+ it "memoizes: warm reads return the same String" do
98
+ doc = ctx.parse("<root><item>v</item></root>")
99
+ tx = doc.root.children.first.children.first
100
+ first = tx.content
101
+ expect(tx.content).to equal(first)
102
+ end
103
+
104
+ it "re-reads after content=" do
105
+ doc = ctx.parse("<root><item>v</item></root>")
106
+ tx = doc.root.children.first.children.first
107
+ tx.content = "changed"
108
+ expect(tx.content).to eq("changed")
109
+ end
110
+
111
+ it "restores entities on entity-bearing documents" do
112
+ ctx2 = Moxml.new(:leptris) { |c| c.restore_entities = true }
113
+ doc = ctx2.parse("<root><item>a &amp; b</item></root>")
114
+ tx = doc.root.children.first.children.first
115
+ bust(tx, :@content)
116
+ expect(tx.content).to eq("a & b")
117
+ end
118
+ end
119
+
120
+ describe "the untouched faces" do
121
+ it "keeps text wrappers named text" do
122
+ doc = ctx.parse("<root><item>v</item></root>")
123
+ tx = doc.root.children.first.children.first
124
+ expect(tx.name).to eq("text")
125
+ end
126
+
127
+ it "keeps element#text on the existing path" do
128
+ doc = ctx.parse("<root><item>v</item></root>")
129
+ el = doc.root.children.first
130
+ expect(el.text).to eq("v")
131
+ end
132
+ end
133
+ 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.145
4
+ version: 0.5.146
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -452,6 +452,7 @@ files:
452
452
  - spec/moxml/adapter/leptris_append_gc_spec.rb
453
453
  - spec/moxml/adapter/leptris_attribute_snapshot_spec.rb
454
454
  - spec/moxml/adapter/leptris_binding_cap_spec.rb
455
+ - spec/moxml/adapter/leptris_contract_reads_spec.rb
455
456
  - spec/moxml/adapter/leptris_doc_parts_spec.rb
456
457
  - spec/moxml/adapter/leptris_document_declaration_spec.rb
457
458
  - spec/moxml/adapter/leptris_inbody_comment_spec.rb