moxml 0.5.0 → 0.5.2

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: 837d647ac39dac23aae2389b6150eafbecf405788f5c62e09af616aa0644715c
4
- data.tar.gz: c7a427e0cf685dd3121e0e5afdfcf388a0d18c52766f22717c8bb8b7d02cecf2
3
+ metadata.gz: 566f12c7bcda29e079d0fd5b496a6becff643b9c15afd328d5f99fb7ef70d35c
4
+ data.tar.gz: a4d7858218db7d38300b963b11d5e40b5aec176d2072a177c4e5448f839b8671
5
5
  SHA512:
6
- metadata.gz: 2e59a3705d492b273708e0d82fa10078be543c4aca86daa7fe7c7c1e90b13ecc774ac89934458b3373d46045e4a2f3fa7eef46172045bbe8ae4fd5105c03030c
7
- data.tar.gz: c8f15cc1577514a368e551444f0e0759170e77e5969a383e0b43025d733e75be8ec99869e43f9fd459f2ac1c0d0c422b26d9e98c73823d76fd18d585d11ebdff
6
+ metadata.gz: 45cf69a5c753718d3bcef44580ef2e8e0dcfa536033537602952e8cbeb73e8bb59e68ad365fdc72c6b7422a998c50e623a1f7b894ed5e7da3cd8e129ff40c703
7
+ data.tar.gz: 0fd18d4891bd29ce56ab1174aa494f8dddc251d298067a0290bf84df41f6854d61bdeedf6e9fd813c32669a9a550419865a637633ddd6e558e84ff0d9f6eca2e
@@ -266,7 +266,13 @@ module Moxml
266
266
  def split_entity_markers(natives, parent)
267
267
  result = []
268
268
  natives.each do |child|
269
- content = child.content.to_s.dup.force_encoding("UTF-8") if child.is_a?(::Leptris::XML::Text)
269
+ # FFI Text#content returns a fresh unfrozen BINARY string:
270
+ # retag in place (dup would be a throwaway allocation), but
271
+ # before include? — BINARY.include? with the UTF-8 marker raises
272
+ if child.is_a?(::Leptris::XML::Text)
273
+ content = child.content
274
+ content.force_encoding("UTF-8")
275
+ end
270
276
  if content&.include?(Entity::MARKER)
271
277
  content.scan(/([^#{Entity::MARKER}]*)(?:#{Entity::MARKER}([\w.:-]+);)?/o) do
272
278
  text_part = Regexp.last_match(1)
@@ -540,6 +546,10 @@ module Moxml
540
546
  # returns those without name accessors). Both engines resolve
541
547
  # expression prefixes against document-declared namespaces.
542
548
  NATIVE_XPATH_CACHE = XPath::Cache.new(100)
549
+ # The gate decision is expression-intrinsic; re-walking the
550
+ # cached AST three times per xpath call cost ~23% of repeated
551
+ # selective queries, so the boolean caches beside it.
552
+ NATIVE_GATE_CACHE = XPath::Cache.new(100)
543
553
 
544
554
  def xpath(node, expression, namespaces = {})
545
555
  native = native_xpath(node, expression, namespaces)
@@ -617,11 +627,13 @@ module Moxml
617
627
  # attribute-node results, or the nokogiri-compat xmlns:
618
628
  # reserved prefix convention.
619
629
  def native_expression?(expression)
620
- ast = XPath::Parser.parse_with_cache(expression)
621
- return false if ast_contains_type?(ast, :variable)
622
- return false if uses_xmlns_prefix?(ast)
630
+ NATIVE_GATE_CACHE.get_or_set(expression) do
631
+ ast = XPath::Parser.parse_with_cache(expression)
632
+ next false if ast_contains_type?(ast, :variable)
633
+ next false if uses_xmlns_prefix?(ast)
623
634
 
624
- !selects_attribute_results?(ast)
635
+ !selects_attribute_results?(ast)
636
+ end
625
637
  rescue XPath::SyntaxError
626
638
  false
627
639
  end
@@ -862,8 +874,23 @@ module Moxml
862
874
 
863
875
  private
864
876
 
877
+ # The variable-length \s* gap after \A defeats Onigmo's anchor
878
+ # optimization, forcing a full-buffer scan — 6.5 ms on a 1 MB
879
+ # document without a declaration (the common case). Match a
880
+ # head slice instead; fall back to the full string only when
881
+ # the head is all whitespace (pathological prefixes).
882
+ SOURCE_DECLARATION_RE = /\A\s*<\?xml\b/
883
+ private_constant :SOURCE_DECLARATION_RE
884
+
865
885
  def record_source_declaration(native_doc, xml_string)
866
- had = xml_string.match?(/\A\s*<\?xml\b/)
886
+ head = xml_string[0, 256]
887
+ had = if head.match?(SOURCE_DECLARATION_RE)
888
+ true
889
+ elsif head.match?(/\A\s*\z/)
890
+ xml_string.match?(SOURCE_DECLARATION_RE)
891
+ else
892
+ false
893
+ end
867
894
  attachments.set(native_doc, :had_source_declaration, had)
868
895
  end
869
896
 
@@ -7,6 +7,11 @@ require "libxml"
7
7
  module Moxml
8
8
  module Adapter
9
9
  class Libxml < Base
10
+ # DOCTYPE prolog sniffer; see parse for the index-then-match
11
+ # strategy that keeps this off the hot path.
12
+ DOCTYPE_RE = /<!DOCTYPE\s+([^\s>]+)(?:\s+PUBLIC\s+"([^"]+)"\s+"([^"]+)"|\s+SYSTEM\s+"([^"]+)")?\s*>/i
13
+ private_constant :DOCTYPE_RE
14
+
10
15
  autoload :EntityRefRegistry, "moxml/adapter/libxml/entity_ref_registry"
11
16
 
12
17
  # Wrapper class to store DOCTYPE information
@@ -89,8 +94,15 @@ module Moxml
89
94
  # parameter or XML declaration for byte interpretation.
90
95
  xml_string = preprocess_entities(xml_string)
91
96
 
92
- # Extract DOCTYPE before parsing
93
- doctype_match = xml_string.match(/<!DOCTYPE\s+([^\s>]+)(?:\s+PUBLIC\s+"([^"]+)"\s+"([^"]+)"|\s+SYSTEM\s+"([^"]+)")?\s*>/i)
97
+ # Extract DOCTYPE before parsing. Locate the literal first:
98
+ # a plain memscan is ~2x cheaper than the regex's literal
99
+ # prefix scan on doctype-less documents (the common case),
100
+ # and matching from the found offset keeps captures
101
+ # identical.
102
+ doctype_idx = xml_string.index("<!DOCTYPE")
103
+ doctype_match = if doctype_idx
104
+ xml_string.match(DOCTYPE_RE, doctype_idx)
105
+ end
94
106
 
95
107
  native_doc = begin
96
108
  # Handle both string and file inputs
@@ -717,28 +717,48 @@ module Moxml
717
717
  # Only documents/elements can contain entity refs or CDATA issues
718
718
  return false unless node.is_a?(::Ox::Document) || node.is_a?(::Ox::Element)
719
719
 
720
- # Check cached flags on documents (most common case)
721
- if node.is_a?(::Ox::Document)
722
- return true if attachments.get(node, :has_entity_refs)
723
- return true if attachments.get(node, :has_cdata_end_markers)
724
- return false if attachments.key?(node, :has_entity_refs) &&
725
- attachments.key?(node, :has_cdata_end_markers)
726
- end
727
-
728
- # Only scan tree on first call — short-circuit on first hit
729
- has_er = tree_has_entity_references?(node)
730
- if has_er
731
- attachments.set(node, :has_entity_refs, true) if node.is_a?(::Ox::Document)
732
- return true
733
- end
734
-
735
- has_cdata = tree_has_cdata_end_markers?(node)
736
- if node.is_a?(::Ox::Document)
737
- attachments.set(node, :has_entity_refs, false)
738
- attachments.set(node, :has_cdata_end_markers, has_cdata)
720
+ # Element serializes consult the owning document's cached
721
+ # flags — a rescan per call made element to_xml ~30x slower
722
+ # than document to_xml on plain documents.
723
+ scan_root = node
724
+ if node.is_a?(::Ox::Element) && (doc = document(node))
725
+ return true if attachments.get(doc, :has_entity_refs)
726
+ return true if attachments.get(doc, :has_cdata_end_markers)
727
+ return false if attachments.key?(doc, :has_entity_refs) &&
728
+ attachments.key?(doc, :has_cdata_end_markers)
729
+
730
+ scan_root = doc
731
+ end
732
+
733
+ # One merged tree scan instead of two; flags cache on the
734
+ # document either way.
735
+ has_er, has_cdata = tree_scan_custom_needs(scan_root)
736
+ if scan_root.is_a?(::Ox::Document)
737
+ attachments.set(scan_root, :has_entity_refs, has_er)
738
+ attachments.set(scan_root, :has_cdata_end_markers, has_cdata)
739
+ end
740
+
741
+ has_er || has_cdata
742
+ end
743
+
744
+ # Single walk collecting both custom-serialize triggers.
745
+ def tree_scan_custom_needs(node)
746
+ has_er = false
747
+ has_cdata = false
748
+ stack = [node]
749
+ until stack.empty?
750
+ current = stack.pop
751
+ case current
752
+ when ::Moxml::Adapter::CustomizedOx::EntityReference
753
+ has_er = true
754
+ when ::Ox::CData
755
+ has_cdata = true if current.value&.include?("]]>")
756
+ when ::Ox::Element, ::Ox::Document
757
+ current.nodes&.each { |child| stack << child }
758
+ end
759
+ return [true, has_cdata] if has_er
739
760
  end
740
-
741
- has_cdata
761
+ [has_er, has_cdata]
742
762
  end
743
763
 
744
764
  def has_declaration?(native_doc, _wrapper)
@@ -186,12 +186,16 @@ module Moxml
186
186
  # Return all children preserving whitespace text nodes
187
187
  result = node.children.dup
188
188
 
189
- # Include any EntityReference wrappers stored alongside native children
189
+ # Include any EntityReference wrappers stored alongside native
190
+ # children. Native children are identity-unique; only this
191
+ # concat can duplicate (the same wrapper added twice), so the
192
+ # dedupe lives here and the common path stays scan-free.
190
193
  entity_refs = attachments.get(node, :entity_refs)
191
- result.concat(entity_refs) if entity_refs
192
-
193
- # Ensure uniqueness by object_id to prevent duplicates
194
- result.uniq(&:object_id)
194
+ if entity_refs
195
+ result.concat(entity_refs).uniq!(&:object_id) || result
196
+ else
197
+ result
198
+ end
195
199
  end
196
200
 
197
201
  def parent(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.0"
4
+ VERSION = "0.5.2"
5
5
  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.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.