moxml 0.5.107 → 0.5.109

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: abbc78c5ee5efd00d9513dbe54fa844d69518c656f2eaec2c16984230ef86e09
4
- data.tar.gz: 2d45e9d308e8c5826d1dafdbeab32e1c0ef1f44dbbb51586283183436b6e42cf
3
+ metadata.gz: 04f63f1599959406386523aafe8289335b021ee5b729b0ac88dec032e08fac55
4
+ data.tar.gz: f4d6cbd0012ef39253c519edd2af087c8ff230b7689622bad6143138e21c9d7d
5
5
  SHA512:
6
- metadata.gz: 3074fb3116eab8bc30697dd0fb75221c10be71f2d2cc884d1954911c7b054ca314db8407a31637fa9c59bd53c00d7bf9b648d436d87cfaec3bffe1102c7af366
7
- data.tar.gz: 3a08135e70f29be37a8c97c75e01239386bd43a4d14c32837525b9395b68ff2778181a1a5499116884eb796bb6b08e30a40509c7d2a5ee25b4056d34ff188dcf
6
+ metadata.gz: f18cb5879bc64127726fc090c2c0f570edb813a032186e883d0c8ac3af7c96ec544f5c3fae7ca3d760b4fb5aba8ac5a1fb2a6a1422e27e7029f074543a036389
7
+ data.tar.gz: a22ade19383fc5da7052f713761fe7bd458e062c59de10e6c8ab00d07f747a00b9627d7503148092630c798359074cbcabb05b91512b1953fd237087beafc2b5
data/README.adoc CHANGED
@@ -655,8 +655,48 @@ root.add_child(title)
655
655
  puts doc.to_xml(indent: 2)
656
656
  ----
657
657
 
658
+ === Performance versus nokogiri
659
+
660
+ CPU-time medians (1,000-record catalog, 20 iterations) through the
661
+ moxml adapter layer on both sides — moxml + leptris 1.9.307 vs
662
+ moxml + nokogiri:
663
+
664
+ [cols="2,1,4"]
665
+ |===
666
+ |Operation |leptris vs nokogiri |Notes
667
+
668
+ |parse
669
+ |3.27x
670
+ |Engine pool-carved parse
671
+
672
+ |serialize
673
+ |3.38x
674
+ |
675
+
676
+ |XPath
677
+ |50x
678
+ |Native compiled-eval seams
679
+
680
+ |walk + read (`.text`, `[]`)
681
+ |1.90x
682
+ |C klass-mint wrappers + wrapper read memos
683
+
684
+ |warm re-read
685
+ |2.10x
686
+ |The memo family — zero string allocation per node
687
+
688
+ |build (`add_element`)
689
+ |4.24x
690
+ |C-mint construction + bulk attributes
691
+
692
+ |attribute pairs
693
+ |1.73x
694
+ |One C crossing, no Attribute objects
695
+ |===
696
+
658
697
  === Fast paths and diagnostics
659
698
 
699
+
660
700
  Read-only attribute listing without Attribute node objects — the
661
701
  walk-hot shape when only names and values are needed (leptris
662
702
  answers in one C crossing; document order, duplicates included):
data/lib/moxml/element.rb CHANGED
@@ -122,8 +122,14 @@ module Moxml
122
122
  # per-attribute objects. Names may be shared frozen strings —
123
123
  # dup before mutating. Mutation and per-attribute namespace
124
124
  # resolution stay on #attributes.
125
+ # One C crossing per read re-allocates the row set (moxml#329);
126
+ # memoized on the attribute-value clock — element-side value
127
+ # writes bump it (see invalidate_attribute_value_cache!).
125
128
  def attribute_pairs
126
- adapter.attribute_pairs(@native)
129
+ generation = attribute_value_generation
130
+ @pairs = nil if @pairs_gen != generation
131
+ @pairs_gen = generation
132
+ @attribute_pairs ||= adapter.attribute_pairs(@native)
127
133
  end
128
134
 
129
135
  # Create an element with attributes and attach it under this
@@ -248,9 +254,18 @@ module Moxml
248
254
  alias add_namespace_definition add_namespace
249
255
 
250
256
  # it's NOT the same as namespaces.first
257
+ # Per-read namespace resolution pays an FFI fetch plus a wrapper
258
+ # mint on every call (moxml#329: a top attribution site in the
259
+ # fallback-path census). Memoized on the namespace-scope clock —
260
+ # every scope mutation bumps it (see invalidate_namespace_cache!).
251
261
  def namespace
252
- ns = adapter.namespace(@native)
253
- ns && Wrappers::Namespace.new(ns, context)
262
+ generation = context.namespace_scope_generation
263
+ @namespace = nil if @namespace_gen != generation
264
+ @namespace_gen = generation
265
+ @namespace ||= begin
266
+ ns = adapter.namespace(@native)
267
+ ns && Wrappers::Namespace.new(ns, context)
268
+ end
254
269
  end
255
270
 
256
271
  # add the prefix to the element name
@@ -72,7 +72,11 @@ module Moxml
72
72
  def group_flat(flat, stride)
73
73
  return EMPTY_ATTRIBUTES if flat.empty?
74
74
 
75
- flat.each_slice(stride).to_a
75
+ # Direct indexing over each_slice: no enumerator chain per
76
+ # record (moxml#329's slice-churn row — the enumerator
77
+ # allocation dominated the sub-array cost on deep trees).
78
+ count = flat.size / stride
79
+ Array.new(count) { |i| flat[i * stride, stride] }
76
80
  end
77
81
  end
78
82
 
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.107"
4
+ VERSION = "0.5.109"
5
5
  end
@@ -155,6 +155,19 @@ module Moxml
155
155
 
156
156
  private
157
157
 
158
+ # After an E/e: a digit, or a sign followed by a digit. Anything
159
+ # else leaves the E as part of a name (1o vs 1e ambiguity guard).
160
+ def lookahead_after_e_is_digit_sign?
161
+ next_char = @expression[@position + 1]
162
+ return true if /\d/.match?(next_char)
163
+
164
+ if /[+-]/.match?(next_char)
165
+ return @expression[@position + 2]&.match?(/\d/) || false
166
+ end
167
+
168
+ false
169
+ end
170
+
158
171
  # Get current character
159
172
  #
160
173
  # @return [String, nil] Current character or nil if at end
@@ -259,6 +272,22 @@ module Moxml
259
272
  end
260
273
  end
261
274
 
275
+ # Exponent part (libxml2/nokogiri extension): digits [Ee] [+|-]
276
+ # digits. Without it, 1E+1 tokenized as 1E followed by + 1 and
277
+ # predicates raised "Expected ']' after predicate".
278
+ if @position < @length && current_char =~ /[Ee]/ &&
279
+ @position + 1 < @length && lookahead_after_e_is_digit_sign?
280
+ value += current_char
281
+ advance
282
+ value += current_char if /[+-]/.match?(current_char)
283
+ advance if /[+-]/.match?(current_char)
284
+
285
+ while @position < @length && current_char =~ /\d/
286
+ value += current_char
287
+ advance
288
+ end
289
+ end
290
+
262
291
  add_token(:number, value, start_pos)
263
292
  end
264
293
 
@@ -451,8 +451,10 @@ module Moxml
451
451
  elsif match?(:number)
452
452
  value = current_value
453
453
  advance
454
- # Convert string to actual numeric value
455
- numeric_value = value.include?(".") ? value.to_f : value.to_i
454
+ # Convert string to actual numeric value. Exponent
455
+ # literals must go through to_f — to_i truncates at the E
456
+ # ("1E+1" became 1).
457
+ numeric_value = value.match?(/[.Ee]/) ? value.to_f : value.to_i
456
458
  return AST::Node.number(numeric_value)
457
459
  elsif match?(:dollar)
458
460
  advance
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Signed exponent literals (libxml2/nokogiri extension): the lexer
6
+ # consumed exponent digits but not their sign, so 1E+1 tokenized as
7
+ # 1E + 1 and predicates raised "Expected ']'"; and the parser's
8
+ # numeric conversion ran to_i on dotless literals, so 2E3 evaluated
9
+ # as 2. Both fixed; the engine now matches nokogiri on every shape.
10
+ RSpec.describe "XPath exponent number literals" do
11
+ %i[nokogiri ox].each do |adapter|
12
+ it "evaluates signed exponents on #{adapter}" do
13
+ ctx = Moxml.new(adapter)
14
+ doc = ctx.parse("<r><x v='2000'/><x v='5'/></r>")
15
+ expect(doc.xpath("//x[@v > 1E+1]").size).to eq(1)
16
+ expect(doc.xpath("//x[@v = 2E3]").size).to eq(1)
17
+ expect(doc.xpath("//x[@v > 5.2E-3]").size).to eq(2)
18
+ expect(doc.xpath("//x[@v < 1e1]").size).to eq(1)
19
+ end
20
+ end
21
+
22
+ it "tokenizes exponent literals as one number" do
23
+ tokens = Moxml::XPath::Lexer.new("1E+1 2E3 5.2E-3 1e2").tokenize
24
+ numbers = tokens.select { |t| t.first == :number }.map { |t| t[1] }
25
+ expect(numbers).to eq(["1E+1", "2E3", "5.2E-3", "1e2"])
26
+ end
27
+
28
+ it "parses exponent literals at full precision" do
29
+ ast = Moxml::XPath::Parser.parse("1E+1")
30
+ literal = ast
31
+ literal = literal.children.first while literal.children.any? { |c| c.respond_to?(:type) }
32
+ expect(literal.value).to eq(10.0)
33
+ end
34
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.107
4
+ version: 0.5.109
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-05 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -557,6 +557,7 @@ files:
557
557
  - spec/moxml/xpath/functions/special_functions_spec.rb
558
558
  - spec/moxml/xpath/functions/string_functions_spec.rb
559
559
  - spec/moxml/xpath/lexer_spec.rb
560
+ - spec/moxml/xpath/number_literals_spec.rb
560
561
  - spec/moxml/xpath/parser_integration_spec.rb
561
562
  - spec/moxml/xpath/parser_spec.rb
562
563
  - spec/moxml/xpath/ruby/generator_spec.rb