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 +4 -4
- data/README.adoc +40 -0
- data/lib/moxml/element.rb +18 -3
- data/lib/moxml/materializer.rb +5 -1
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/lexer.rb +29 -0
- data/lib/moxml/xpath/parser.rb +4 -2
- data/spec/moxml/xpath/number_literals_spec.rb +34 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04f63f1599959406386523aafe8289335b021ee5b729b0ac88dec032e08fac55
|
|
4
|
+
data.tar.gz: f4d6cbd0012ef39253c519edd2af087c8ff230b7689622bad6143138e21c9d7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
data/lib/moxml/materializer.rb
CHANGED
|
@@ -72,7 +72,11 @@ module Moxml
|
|
|
72
72
|
def group_flat(flat, stride)
|
|
73
73
|
return EMPTY_ATTRIBUTES if flat.empty?
|
|
74
74
|
|
|
75
|
-
|
|
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
data/lib/moxml/xpath/lexer.rb
CHANGED
|
@@ -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
|
|
data/lib/moxml/xpath/parser.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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-
|
|
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
|