moxml 0.5.30 → 0.5.31

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.
@@ -923,12 +923,23 @@ module Moxml
923
923
  native.name
924
924
  end
925
925
 
926
+ # libxml-ruby 5.x exposes no external/system ID accessor on
927
+ # the DTD node — the DOCTYPE source text is the only source,
928
+ # so re-derive from it (same DOCTYPE_RE as the parse path).
926
929
  def doctype_external_id(native)
927
- native.external_id
930
+ if native.is_a?(DoctypeWrapper)
931
+ native.external_id
932
+ else
933
+ native.to_s[DOCTYPE_RE, 2]
934
+ end
928
935
  end
929
936
 
930
937
  def doctype_system_id(native)
931
- native.system_id
938
+ if native.is_a?(DoctypeWrapper)
939
+ native.system_id
940
+ else
941
+ native.to_s[DOCTYPE_RE, 3] || native.to_s[DOCTYPE_RE, 4]
942
+ end
932
943
  end
933
944
 
934
945
  def xpath(node, expression, namespaces = nil)
@@ -12,12 +12,50 @@ module Moxml
12
12
  @attachments ||= Moxml::NativeAttachment.new
13
13
  end
14
14
 
15
+ def bare_set_qname_safe?
16
+ true
17
+ end
18
+
19
+ # Qualified-name reads are clean on this engine (verified by
20
+ # the expanded-name specs); markers restore in-line when the
21
+ # parse recorded them.
22
+ def bare_get_qname_safe?
23
+ true
24
+ end
25
+
26
+ # Fast bare-name read for Element#[]: the native call plus,
27
+ # only when the parse recorded entity markers, their
28
+ # restoration (the resolver path's other real semantic).
29
+ # The marker flag is constant per document — a WeakMap on
30
+ # the adapter beats the per-read document fetch + attachment
31
+ # hash chain.
32
+ def doc_entity_markers?(doc)
33
+ cache = (@doc_markers ||= ObjectSpace::WeakMap.new)
34
+ cached = cache[doc]
35
+ return cached unless cached.nil?
36
+
37
+ cache[doc] = attachments.get(doc, :entity_markers) == true
38
+ end
39
+
40
+ def bare_attr_value(element, name)
41
+ value = element[name.to_s]
42
+ if value.is_a?(String) && doc_entity_markers?(element.document)
43
+ restore_entities(value)
44
+ else
45
+ value
46
+ end
47
+ end
48
+
49
+ def native_identity_stable?
50
+ true
51
+ end
52
+
15
53
  def set_root(doc, element)
16
54
  doc.root = element
17
55
  end
18
56
 
19
57
  def parse(xml, options = {}, _context = nil)
20
- processed_xml = preprocess_entities(xml)
58
+ processed_xml, entity_markers = Entity.preprocess_with_marker_flag(xml)
21
59
 
22
60
  # preprocess_entities always returns UTF-8, so tell Nokogiri to
23
61
  # parse as UTF-8 regardless of any original encoding option.
@@ -42,9 +80,20 @@ module Moxml
42
80
 
43
81
  # Use provided context if available, otherwise create new one
44
82
  ctx = _context || Context.new(:nokogiri)
83
+ attachments.set(native_doc, :entity_markers, entity_markers)
45
84
  Document.new(native_doc, ctx)
46
85
  end
47
86
 
87
+ # Tolerant HTML parsing through libxml2's HTML mode: implied
88
+ # html/head/body structure, void elements, lowercased names,
89
+ # the HTML named-entity table. Recovery is inherent —
90
+ # malformed input never raises.
91
+ def parse_html(html, _options = {}, _context = nil)
92
+ html_string = html.is_a?(IO) || html.is_a?(StringIO) ? html.read : html.to_s
93
+ native_doc = ::Nokogiri::HTML(html_string)
94
+ Document.new(native_doc, _context || Context.new(:nokogiri))
95
+ end
96
+
48
97
  # Nokogiri parses with `config.recover` unless strict, so
49
98
  # recoverable syntax errors land on `doc.errors` instead of
50
99
  # raising (issue #147).
@@ -386,9 +435,11 @@ module Moxml
386
435
 
387
436
  def xpath(node, expression, namespaces = nil)
388
437
  result = node.xpath(expression, namespaces)
389
- # Adapter contract: Array<native> | scalar (count(),
390
- # string-length(), boolean functions return scalars).
391
- result.is_a?(Enumerable) ? result.to_a : result
438
+ # Adapter contract: Array<native> | LazyNodeSet | scalar
439
+ # (count(), string-length(), boolean functions return
440
+ # scalars). The native node-set passes through lazily —
441
+ # to_a would materialize every result wrapper up front.
442
+ result.is_a?(Enumerable) ? result.extend(Moxml::LazyNodeSet) : result
392
443
  rescue ::Nokogiri::XML::XPath::SyntaxError => e
393
444
  raise Moxml::XPathError.new(
394
445
  e.message,
@@ -15,6 +15,14 @@ module Moxml
15
15
  @attachments ||= Moxml::NativeAttachment.new
16
16
  end
17
17
 
18
+ def bare_set_qname_safe?
19
+ true
20
+ end
21
+
22
+ def native_identity_stable?
23
+ true
24
+ end
25
+
18
26
  def set_root(doc, element)
19
27
  existing_root = root(doc)
20
28
  element.parent = doc if element.is_a?(::Ox::Node)
@@ -14,6 +14,14 @@ module Moxml
14
14
  @attachments ||= Moxml::NativeAttachment.new
15
15
  end
16
16
 
17
+ def bare_set_qname_safe?
18
+ true
19
+ end
20
+
21
+ def native_identity_stable?
22
+ true
23
+ end
24
+
17
25
  def parse(xml, options = {}, _context = nil)
18
26
  xml = "" if xml.nil?
19
27
 
@@ -29,6 +29,27 @@ module Moxml
29
29
  module_function
30
30
 
31
31
  # @return [Moxml::Attribute, nil] the matching attribute wrapper
32
+ # Value-only prefixed resolution for Element#[]: prefix
33
+ # resolution is the resolver's real work; the match step goes to
34
+ # the engine's expanded-name lookup where the adapter offers it,
35
+ # skipping the attribute-list materialization and per-attr URI
36
+ # probes. Bare names take Element#'s adapter fast path instead.
37
+ def resolve_value(element, name)
38
+ prefix, local = name.split(":", 2)
39
+ return nil if prefix == "xmlns"
40
+
41
+ uri = prefix_uri(element, prefix)
42
+ return nil if uri.nil?
43
+
44
+ adapter = element.context.config.adapter
45
+ if adapter.is_a?(Moxml::Adapter::Leptris)
46
+ return adapter.expanded_attr_value(element.native, uri, local)
47
+ end
48
+
49
+ attr = resolve(element, name)
50
+ attr&.value
51
+ end
52
+
32
53
  def resolve(element, name)
33
54
  name = name.to_s
34
55
  if name.include?(":")
@@ -91,6 +112,20 @@ module Moxml
91
112
  return value
92
113
  end
93
114
 
115
+ # Fast path: every adapter's set_attribute writes by qualified
116
+ # name, and XML cannot namespace an unprefixed attribute — so
117
+ # a bare-name set replaces only the no-namespace attribute and
118
+ # can never touch a namespaced p:<local> sibling. The full
119
+ # expanded-name resolve (wrap every attribute, resolve each
120
+ # prefix against in-scope namespaces) is only needed for
121
+ # prefixed names; it dominated programmatic-builder attribute
122
+ # writes.
123
+ if !name.include?(":") && adapter.bare_set_qname_safe?
124
+ adapter.set_attribute(element.native, name, value)
125
+ element.invalidate_attribute_cache!
126
+ return value
127
+ end
128
+
94
129
  existing = resolve(element, name)
95
130
  if existing
96
131
  existing.value = value
data/lib/moxml/c14n.rb CHANGED
@@ -39,6 +39,11 @@ module Moxml
39
39
  # :exclusive10 Exclusive C14N 1.0 (W3C REC-xml-exc-c14n-20020718)
40
40
  def self.canonicalize(node_or_xml, with_comments: false,
41
41
  algorithm: :inclusive10, inclusive_namespaces: [])
42
+ if (native = native_inclusive10(node_or_xml, with_comments,
43
+ inclusive_namespaces))
44
+ return native
45
+ end
46
+
42
47
  engine_for(algorithm).canonicalize(
43
48
  node_or_xml,
44
49
  with_comments: with_comments,
@@ -46,6 +51,31 @@ module Moxml
46
51
  )
47
52
  end
48
53
 
54
+ # The leptris engine canonicalizes in C — 23x the Ruby reference.
55
+ # Delegated only for the exact default shape (Inclusive 1.0, no
56
+ # comments, no InclusiveNamespace prefix list) on wrappers whose
57
+ # adapter passed the NATIVE_C14N_BYTE_SAFE load probe; every
58
+ # other combination keeps the Ruby engine (node-set subsets,
59
+ # xml:base fixup, exclusive, 1.1 stay on the ported
60
+ # implementation regardless).
61
+ def self.native_inclusive10(node_or_xml, with_comments, inclusive_namespaces)
62
+ return nil if with_comments || !inclusive_namespaces.empty?
63
+ return nil unless algorithm_shape_inclusive10?(node_or_xml)
64
+
65
+ adapter = node_or_xml.context.config.adapter
66
+ return nil unless adapter == Moxml::Adapter::Leptris &&
67
+ Moxml::Adapter::Leptris.native_c14n_byte_safe?
68
+
69
+ node_or_xml.native.canonicalize(
70
+ ::Leptris::XML::FFI::C14N_1_0, nil,
71
+ mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
72
+ )
73
+ end
74
+
75
+ def self.algorithm_shape_inclusive10?(node_or_xml)
76
+ node_or_xml.is_a?(Moxml::Element) || node_or_xml.is_a?(Moxml::Document)
77
+ end
78
+
49
79
  def self.canonicalize_inclusive10(node_or_xml, with_comments: false)
50
80
  Inclusive10.new.canonicalize(node_or_xml, with_comments: with_comments)
51
81
  end
data/lib/moxml/context.rb CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Moxml
4
4
  class Context
5
+ # Opal's default runtime excludes ObjectSpace entirely; the weak
6
+ # registry needs WeakMap, so the strong Hash (with its
7
+ # wholesale-clear valve) covers that platform.
8
+ WEAK_WRAPPERS = !defined?(ObjectSpace::WeakMap).nil?
5
9
  attr_reader :config
6
10
 
7
11
  def initialize(adapter = nil)
@@ -14,7 +18,12 @@ module Moxml
14
18
  # the same wrapper instead of allocating a fresh one per
15
19
  # access. Keyed by object identity; re-keyed by
16
20
  # Node#refresh_native! when an adapter swaps a native.
17
- @wrappers = {}.compare_by_identity
21
+ # WeakMap where available: entries die with their native —
22
+ # parse-and-drop workloads release wrappers, natives, and (via
23
+ # the binding's finalizer) the C subtrees, instead of pinning
24
+ # up to the old 65,536 wholesale-clear valve which also
25
+ # destroyed identity for live wrappers when it fired.
26
+ @wrappers = WEAK_WRAPPERS ? ObjectSpace::WeakMap.new : {}.compare_by_identity
18
27
  end
19
28
 
20
29
  def wrapper_for(native)
@@ -22,18 +31,26 @@ module Moxml
22
31
  end
23
32
 
24
33
  def register_wrapper(native, wrapper)
25
- # Safety valve + adapter opt-in. Adapters whose natives are
26
- # recreated per access (libxml mints fresh Ruby objects for the
27
- # same C node) opt out so the map does not accumulate dead
28
- # entries; the default is opt-in.
34
+ # Adapter opt-in. Adapters whose natives are recreated per
35
+ # access (libxml mints fresh Ruby objects for the same C node)
36
+ # opt out so the map does not accumulate dead entries; the
37
+ # default is opt-in.
29
38
  return if @config&.adapter&.wrappers_recyclable? == false
30
39
 
31
- @wrappers.clear if @wrappers.size >= 65_536
40
+ # The strong fallback (Opal) needs the wholesale-clear valve;
41
+ # the WeakMap registry is self-cleaning.
42
+ @wrappers.clear if @wrappers.is_a?(Hash) && @wrappers.size >= 65_536
32
43
  @wrappers[native] = wrapper
33
44
  end
34
45
 
46
+ # WeakMap has no delete — a nil value tombstones the entry (reads
47
+ # as a miss) and dies with the native like any other.
35
48
  def unregister_wrapper(native)
36
- @wrappers.delete(native)
49
+ if WEAK_WRAPPERS
50
+ @wrappers[native] = nil
51
+ else
52
+ @wrappers.delete(native)
53
+ end
37
54
  end
38
55
 
39
56
  def namespace_scope_generation
@@ -74,6 +91,29 @@ module Moxml
74
91
  doc
75
92
  end
76
93
 
94
+ # Tolerant HTML4/5 parsing into the standard DOM: implied end
95
+ # tags, void elements, case-insensitive lowercased names, the
96
+ # HTML named-entity table, synthesized html/head/body. Supported
97
+ # by adapters with an engine HTML mode (leptris >= 1.9.80,
98
+ # nokogiri); others raise Moxml::AdapterError.
99
+ def parse_html(html, options = {})
100
+ config.adapter.parse_html(html, options, self)
101
+ end
102
+
103
+ # Streaming incremental parse (leptris): yields each completed
104
+ # element while parsing — :top_level yields the root's children,
105
+ # :full_document every element in completion (post-order) order.
106
+ # Memory stays bounded by the largest subtree, not the document
107
+ # (iterparse_file streams the file C-side). Yielded elements are
108
+ # parentless and valid only inside the block.
109
+ def iterparse(xml, mode: :top_level, &block)
110
+ config.adapter.iterparse(xml, mode, self, &block)
111
+ end
112
+
113
+ def iterparse_file(path, mode: :top_level, &block)
114
+ config.adapter.iterparse_file(path, mode, self, &block)
115
+ end
116
+
77
117
  # Parse then flatten in one call — see Moxml::Materializer
78
118
  # (issue #132). Yields records; returns an Enumerator when no
79
119
  # block is given.
@@ -128,6 +168,26 @@ module Moxml
128
168
  Builder.new(self).build(&block)
129
169
  end
130
170
 
171
+ # Frozen serialization defaults, memoized per context — Node#to_xml
172
+ # merged these from live config reads on every call (bulk element
173
+ # serialization pays a hash build + 4 chain derefs per element).
174
+ def default_serialize_options
175
+ @default_serialize_options ||= {
176
+ encoding: config.default_encoding,
177
+ indent: config.default_indent,
178
+ line_ending: config.default_line_ending,
179
+ expand_empty: true,
180
+ }.freeze
181
+ end
182
+
183
+ # The argless element.to_xml form resolves every option statically
184
+ # (no declaration for non-document nodes, context defaults for the
185
+ # rest) — this prebuilt frozen hash makes that path allocation-free.
186
+ def default_element_serialize_options
187
+ @default_element_serialize_options ||=
188
+ default_serialize_options.merge(no_declaration: true).freeze
189
+ end
190
+
131
191
  private
132
192
 
133
193
  def build_entity_registry
@@ -131,11 +131,11 @@ module Moxml
131
131
  # Handle different result types:
132
132
  # - Scalar values (from functions): return directly
133
133
  # - NodeSet: already wrapped, return directly
134
- # - Array: wrap in NodeSet
134
+ # - Array / LazyNodeSet: wrap in NodeSet
135
135
  case result
136
136
  when NodeSet, Float, String, TrueClass, FalseClass, NilClass
137
137
  result
138
- when Array
138
+ when Array, LazyNodeSet
139
139
  NodeSet.new(result, context)
140
140
  else
141
141
  # For other types, try to wrap in NodeSet
data/lib/moxml/element.rb CHANGED
@@ -46,7 +46,9 @@ module Moxml
46
46
  # attributes only; prefixed names replace by namespace URI and
47
47
  # require a declared prefix. Cache coherence is the resolver's.
48
48
  def []=(name, value)
49
- context.bump_namespace_scope_generation
49
+ # Every assign path (native set or value=) ends in
50
+ # invalidate_attribute_cache!, which bumps the generation —
51
+ # a leading bump here was one per write.
50
52
  Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
51
53
  end
52
54
 
@@ -54,12 +56,25 @@ module Moxml
54
56
  # bare names match only no-namespace attributes; prefixed names
55
57
  # resolve through in-scope declarations to expanded names.
56
58
  def [](name)
57
- cache = attribute_read_cache
58
59
  key = name.to_s
60
+ # Bare names on qualified-name engines: one adapter call with
61
+ # the adapter's own marker restoration (bare_attr_value). The
62
+ # resolver's expanded-name semantics only pay for prefixed
63
+ # names, where resolution is real work.
64
+ adapter = self.adapter
65
+ if !key.include?(":") && adapter.bare_get_qname_safe?
66
+ return adapter.bare_attr_value(@native, key)
67
+ end
68
+
69
+ cache = attribute_read_cache
59
70
  unless cache.key?(key)
60
- cache[key] = Moxml::AttributeResolver.resolve(self, key)
71
+ cache[key] = if key.include?(":")
72
+ Moxml::AttributeResolver.resolve_value(self, key)
73
+ else
74
+ Moxml::AttributeResolver.resolve(self, key)&.value
75
+ end
61
76
  end
62
- cache[key]&.value
77
+ cache[key]
63
78
  end
64
79
 
65
80
  def attribute(name)
@@ -92,11 +107,41 @@ module Moxml
92
107
  end
93
108
 
94
109
  def remove_attribute(name)
95
- context.bump_namespace_scope_generation
110
+ # Both remove paths end in invalidate_attribute_cache!, which
111
+ # bumps the generation.
96
112
  Moxml::AttributeResolver.remove(self, name)
97
113
  self
98
114
  end
99
115
 
116
+ # Append a raw XML fragment's top-level nodes as children.
117
+ #
118
+ # Bulk construction rides the engine's parser instead of
119
+ # per-node create/attach calls — measured 2.0x faster than
120
+ # per-node building through the same adapter, and faster than
121
+ # per-node building on Nokogiri (the parse is C; the per-node
122
+ # path pays an FFI crossing per node).
123
+ #
124
+ # The fragment must be namespace-self-contained (declarations
125
+ # inside it ride along with the moved subtrees; prefixes relying
126
+ # on THIS element's scope must be spelled out in the fragment)
127
+ # and well-formed as the content of a single wrapper element —
128
+ # declarations and doctypes raise ParseError.
129
+ #
130
+ # @return [Moxml::Element] self
131
+ def append_xml(fragment)
132
+ if context.config.adapter_name == :ox
133
+ raise Moxml::AdapterError.new(
134
+ "append_xml is not supported by the Ox adapter (its customized node wrappers do not survive cross-document attachment)",
135
+ adapter: "Ox", operation: "append_xml",
136
+ )
137
+ end
138
+
139
+ wrapper = context.parse("<m>#{fragment}</m>")
140
+ children = wrapper.root.children.to_a
141
+ children.each { |child| add_child(child) }
142
+ self
143
+ end
144
+
100
145
  def add_namespace(prefix, uri)
101
146
  adapter.create_namespace(@native, prefix, uri,
102
147
  namespace_validation_mode: context.config.namespace_validation_mode)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # Marker for an adapter's native node-set result that NodeSet can
5
+ # hold without materializing the natives. The leptris binding's
6
+ # NodeSet mints one Ruby wrapper per result node in #to_a — the
7
+ # whole 900-node result set is built even when the caller only
8
+ # asks .size or .first — so the adapter hands the native set
9
+ # through instead. Contract: responds to length, empty?, [],
10
+ # each, to_a (yielding natives).
11
+ module LazyNodeSet
12
+ def size
13
+ length
14
+ end
15
+ end
16
+ end
@@ -25,8 +25,13 @@ module Moxml
25
25
  @monitor = Monitor.new
26
26
  end
27
27
 
28
+ # Reads are lock-free: MRI Hash#[] is atomic under the GVL, and
29
+ # the only mid-write race (set assigns the inner hash before its
30
+ # key) resolves to a miss — the same answer as "not set yet",
31
+ # which every reader treats conservatively. This method sits on
32
+ # per-element hot paths (entity_bearing? on every serialize).
28
33
  def get(native, key)
29
- @monitor.synchronize { @data[native.object_id]&.[](key) }
34
+ @data[native.object_id]&.[](key)
30
35
  end
31
36
 
32
37
  def set(native, key, value)
@@ -39,7 +44,8 @@ module Moxml
39
44
  end
40
45
 
41
46
  def key?(native, key)
42
- @monitor.synchronize { @data[native.object_id]&.key?(key) || false }
47
+ h = @data[native.object_id]
48
+ !h.nil? && h.key?(key)
43
49
  end
44
50
 
45
51
  def delete(native, key)
data/lib/moxml/node.rb CHANGED
@@ -35,6 +35,7 @@ module Moxml
35
35
  def clear_native_memo!
36
36
  @name = nil
37
37
  @attribute_cache = nil
38
+ @entity_bearing_gen = nil
38
39
  end
39
40
 
40
41
  def document
@@ -64,9 +65,12 @@ module Moxml
64
65
  def add_child(node)
65
66
  node = prepare_node(node)
66
67
  adapter.add_child(@native, node.native)
67
- # Refresh native in case adapter changed identity (e.g., LibXML doc.root=)
68
- refreshed = adapter.actual_native(node.native, @native)
69
- node.refresh_native!(refreshed) if refreshed && refreshed != node.native
68
+ # Refresh native in case adapter changed identity (e.g., LibXML
69
+ # doc.root=); stable-identity adapters skip the round trip.
70
+ unless adapter.native_identity_stable?
71
+ refreshed = adapter.actual_native(node.native, @native)
72
+ node.refresh_native!(refreshed) if refreshed && refreshed != node.native
73
+ end
70
74
  node.parent_node = self
71
75
  # The adopted subtree's in-scope namespaces changed
72
76
  node.invalidate_namespace_cache!
@@ -113,23 +117,43 @@ module Moxml
113
117
  # Determine if we should include XML declaration
114
118
  # For Document nodes: check native then wrapper, unless explicitly overridden
115
119
  # For other nodes: default to no declaration unless explicitly set
116
- serialize_options = default_options.merge(options)
117
- serialize_options[:no_declaration] = !should_include_declaration?(options)
120
+ serialize_options = if options.empty? && !is_a?(Document)
121
+ context.default_element_serialize_options
122
+ else
123
+ merged = context.default_serialize_options.merge(options)
124
+ merged[:no_declaration] = !should_include_declaration?(options)
125
+ merged
126
+ end
118
127
 
119
128
  result = adapter.serialize(@native, serialize_options)
120
129
  result = apply_line_ending(result, serialize_options[:line_ending])
121
130
 
122
131
  # Restore entity markers to named entity references; skipped
123
132
  # when the adapter knows the document carries no markers.
124
- result = adapter.restore_entities(result) if adapter.entity_bearing?(@native)
133
+ result = adapter.restore_entities(result) if entity_bearing?
125
134
  result
126
135
  end
127
136
 
137
+ # Memoized against the adapter's serialize generation — the
138
+ # entity-marker flag flips at parse and entity-reference mint,
139
+ # both adapter-level, and the generation bump is the invalidation
140
+ # signal. Adapters with static answers (base class) never bump.
141
+ def entity_bearing?
142
+ gen = adapter.serialize_generation
143
+ if @entity_bearing_gen == gen
144
+ @entity_bearing
145
+ else
146
+ @entity_bearing_gen = gen
147
+ @entity_bearing = adapter.entity_bearing?(@native)
148
+ end
149
+ end
150
+
128
151
  def xpath(expression, namespaces = {})
129
152
  result = adapter.xpath(@native, expression, namespaces)
130
- # Adapter contract: Array<native> | scalar. Scalars (count(),
131
- # string-length(), booleans) pass through unwrapped.
132
- result.is_a?(Array) ? NodeSet.new(result, context) : result
153
+ # Adapter contract: Array<native> | LazyNodeSet | scalar.
154
+ # Scalars (count(), string-length(), booleans) pass through
155
+ # unwrapped; the set forms wrap lazily.
156
+ result.is_a?(Array) || result.is_a?(LazyNodeSet) ? NodeSet.new(result, context) : result
133
157
  end
134
158
 
135
159
  # Flattened post-order records for this subtree without
@@ -459,19 +483,6 @@ module Moxml
459
483
  end
460
484
  end
461
485
 
462
- def default_options
463
- {
464
- encoding: context.config.default_encoding,
465
- indent: context.config.default_indent,
466
- line_ending: context.config.default_line_ending,
467
- # The short format of empty tags in Oga and Nokogiri isn't configurable
468
- # Oga: <empty /> (with a space)
469
- # Nokogiri: <empty/> (without a space)
470
- # The expanded format is enforced to avoid this conflict
471
- expand_empty: true,
472
- }
473
- end
474
-
475
486
  def should_include_declaration?(options)
476
487
  return options[:declaration] if options.key?(:declaration)
477
488
  return options.fetch(:declaration, false) unless is_a?(Document)