moxml 0.5.19 → 0.5.21

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: 62f62c2c744354f3dc8c6436cd5404eac78738bc3da00509c1daed9e827d5195
4
- data.tar.gz: 25cddfe9066c8ebda91925a47834fb3180aaefc2b7be5f74b32409a3bdd44ffb
3
+ metadata.gz: efd15ef624c00bcb6215f7a120272833f45f4c0f297d2a98b19507540878eec8
4
+ data.tar.gz: 640713084bd178087975387abca9cec4a681c2e94ce905ac16becb5514e25ca2
5
5
  SHA512:
6
- metadata.gz: 4598f34a5af3aa247349f0bf0cd5ea99fceb2a60c02412b31b3e6ecc391f0e5292668b5ca9c8bb2be304dee1cc7e3706e932ffe3c73ceaa9e0f9d45476c4fa24
7
- data.tar.gz: e7c39fcc1a5dad848d9b4484e6065b3af1a890a8b4085a5d7cf731d20010a480d6ef0e2c6f8cbd7a579cc9c524360364475c2fb29c1704632fa7472a25a2eabc
6
+ metadata.gz: 2d418bb84689e70721a4e9b16359136e94d781d7da0bc2c9b7374f58ff911d08fff0eb874a8ed31a6434b9d953381714ad8d1dad5911a6b457b8ae6143a53491
7
+ data.tar.gz: 6aca55287d55514bd1dfc52c4286909760fb2b9230c33989c21bda4b78d873977ca07ef30df165413d959a42cd7ffa052677aaae672dfe1884f6e84af3b270c5
@@ -76,6 +76,9 @@ module Moxml
76
76
  # only walks the root subtree, so declaration, DOCTYPE, PIs and
77
77
  # document-level text are assembled around it explicitly.
78
78
  def serialize_document(doc, options)
79
+ fast = fast_document_output(doc, options)
80
+ return fast if fast
81
+
79
82
  # Nokogiri's document shape: every top-level part is
80
83
  # newline-terminated, at any indent — declaration, DOCTYPE,
81
84
  # document PIs, the root element, trailing newline after it.
@@ -108,6 +111,69 @@ module Moxml
108
111
  parts.join
109
112
  end
110
113
 
114
+ # Issue #158: the engine's whole-document serializer is one C
115
+ # call; the composed path serializes the root through the
116
+ # element face, which copies the subtree into a fresh document
117
+ # on every call (~4.5x slower end to end). The engine's output
118
+ # is byte-identical to the composed one EXCEPT epilog parts
119
+ # glue directly to the root — so the fast path declines
120
+ # whenever anything follows the root, any attachment overrides
121
+ # a part, or the engine's declaration line differs from the
122
+ # facade's canonical form (checked post-hoc: a source
123
+ # declaration carrying standalone or another version would
124
+ # diverge).
125
+ def fast_document_output(doc, options)
126
+ return nil unless LIBXML2_LAYOUT_PARITY
127
+ return nil if attachments.get(doc, :declaration) ||
128
+ attachments.get(doc, :doctype) ||
129
+ attachments.get(doc, :document_text) ||
130
+ attachments.get(doc, :entity_markers)
131
+
132
+ include_decl = !options[:no_declaration] && options.fetch(:declaration) do
133
+ document_has_declaration?(doc)
134
+ end
135
+
136
+ root_seen = false
137
+ doc.children.each do |child|
138
+ if child.is_a?(::Leptris::XML::Element)
139
+ return nil if root_seen
140
+
141
+ root_seen = true
142
+ elsif root_seen
143
+ # Epilog parts glue directly to the root in the engine's
144
+ # document output — compose those.
145
+ return nil
146
+ end
147
+ end
148
+
149
+ # The engine's subset serializer mangles every declaration
150
+ # after the first (loses its "<") — moxml's own formatter is
151
+ # correct, so multi-declaration subsets compose.
152
+ dt = doc.doctype
153
+ if dt&.class&.method_defined?(:internal_subset) &&
154
+ (subset = dt.internal_subset) && subset.scan("<!").size > 1
155
+ return nil
156
+ end
157
+
158
+ kwargs = {
159
+ indent: options.fetch(:indent, 0),
160
+ no_decl: !include_decl,
161
+ encoding: options[:encoding],
162
+ }
163
+ if INDENT_UNIT_SUPPORTED && options[:indent_text].is_a?(String)
164
+ kwargs[:indent_text] = options[:indent_text]
165
+ end
166
+ output = doc.to_xml(**kwargs)
167
+ return nil if output.nil? || output.empty?
168
+
169
+ if include_decl && !output.start_with?(default_declaration_xml(doc, options))
170
+ return nil
171
+ end
172
+
173
+ output << "\n" unless output.end_with?("\n")
174
+ output
175
+ end
176
+
111
177
  def native_doctype_xml(doc)
112
178
  dt = doc.doctype
113
179
  return nil unless dt
data/lib/moxml/entity.rb CHANGED
@@ -43,6 +43,12 @@ module Moxml
43
43
  SERIALIZED_MARKER_RE = /&#xFFFC;&#xFEFF;(#{NAME_PATTERN});/
44
44
  STANDARD_ENTITIES = %w[amp lt gt quot apos].freeze
45
45
 
46
+ # One regex pass, no allocations: true when a NON-standard named
47
+ # entity exists somewhere. Documents carrying only the five
48
+ # predefined entities (the overwhelmingly common case) skip the
49
+ # marker gsub's full-buffer copy entirely.
50
+ NON_STANDARD_ENTITY_RE = /&(?!amp;|lt;|gt;|quot;|apos;)(#{NAME_PATTERN});/
51
+
46
52
  NAMED_DECODE_MAP = {
47
53
  "amp" => "&", "lt" => "<", "gt" => ">",
48
54
  "quot" => '"', "apos" => "'"
@@ -82,6 +88,10 @@ module Moxml
82
88
  # the regex scan and string allocation entirely. The vast
83
89
  # majority of XML payloads contain no entity references.
84
90
  return [str, false] unless str.include?("&")
91
+ # Second fast path: only predefined entities — the gsub would
92
+ # pass every match through unchanged, so skip its full-buffer
93
+ # copy too.
94
+ return [str, false] unless str.match?(NON_STANDARD_ENTITY_RE)
85
95
 
86
96
  marked = false
87
97
  processed = str.gsub(NAME_RE) do |match|
data/lib/moxml/node.rb CHANGED
@@ -339,10 +339,18 @@ module Moxml
339
339
 
340
340
  TYPES.each do |node_type|
341
341
  define_method "#{node_type}?" do
342
- adapter.node_type(native) == node_type
342
+ node_type_cached == node_type
343
343
  end
344
344
  end
345
345
 
346
+ # The adapter's type probe is an FFI call per invocation; the
347
+ # type is fixed for a node's lifetime, so the first answer is
348
+ # memoized on the wrapper.
349
+ def node_type_cached
350
+ @node_type_cached ||= adapter.node_type(@native)
351
+ end
352
+ private :node_type_cached
353
+
346
354
  # Returns the primary identifier for this node type
347
355
  # For Element: the tag name
348
356
  # For Attribute: the attribute name
@@ -392,7 +400,9 @@ module Moxml
392
400
  protected
393
401
 
394
402
  def adapter
395
- context.config.adapter
403
+ # A context's adapter object is fixed for its lifetime; the
404
+ # chain deref ran on every node access.
405
+ @adapter ||= context.config.adapter
396
406
  end
397
407
 
398
408
  def self.adapter(context)
@@ -23,9 +23,16 @@ module Moxml
23
23
  def each
24
24
  return to_enum(:each) unless block_given?
25
25
 
26
- @nodes.each_with_index do |node, i|
27
- @wrapped[i] ||= wrap_with_parent(node)
28
- yield @wrapped[i]
26
+ wrapped = @wrapped
27
+ index = 0
28
+ @nodes.each do |node|
29
+ wrapper = wrapped[index]
30
+ unless wrapper
31
+ wrapper = wrap_with_parent(node)
32
+ wrapped[index] = wrapper
33
+ end
34
+ yield wrapper
35
+ index += 1
29
36
  end
30
37
  self
31
38
  end
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.19"
4
+ VERSION = "0.5.21"
5
5
  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.19
4
+ version: 0.5.21
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-08-31 00:00:00.000000000 Z
11
+ date: 2026-09-01 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