moxml 0.1.18 → 0.1.20

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +181 -11
  3. data/README.adoc +24 -1
  4. data/docs/_guides/node-api-consistency.adoc +4 -0
  5. data/lib/moxml/adapter/base.rb +6 -1
  6. data/lib/moxml/adapter/customized_ox/text.rb +15 -2
  7. data/lib/moxml/adapter/customized_rexml/formatter.rb +1 -0
  8. data/lib/moxml/adapter/libxml.rb +7 -2
  9. data/lib/moxml/adapter/oga.rb +6 -2
  10. data/lib/moxml/adapter/ox.rb +15 -8
  11. data/lib/moxml/builder.rb +12 -5
  12. data/lib/moxml/config.rb +1 -1
  13. data/lib/moxml/entity_registry.rb +1 -0
  14. data/lib/moxml/native_attachment/native.rb +69 -0
  15. data/lib/moxml/native_attachment/opal.rb +34 -0
  16. data/lib/moxml/native_attachment.rb +16 -46
  17. data/lib/moxml/text.rb +4 -0
  18. data/lib/moxml/version.rb +1 -1
  19. data/lib/moxml/xpath/compiler.rb +2 -1
  20. data/spec/integration/shared_examples/edge_cases.rb +4 -2
  21. data/spec/integration/shared_examples/entity_reference_whitespace.rb +1 -1
  22. data/spec/integration/shared_examples/high_level/document_builder_behavior.rb +3 -1
  23. data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +10 -4
  24. data/spec/integration/shared_examples/node_wrappers/node_set_behavior.rb +1 -1
  25. data/spec/moxml/adapter/headed_ox_spec.rb +1 -1
  26. data/spec/moxml/lazy_parse_spec.rb +1 -1
  27. data/spec/moxml/moxml_spec.rb +6 -40
  28. data/spec/moxml/native_attachment/native_spec.rb +57 -0
  29. data/spec/moxml/native_attachment/opal_spec.rb +29 -0
  30. data/spec/moxml/native_attachment/shared_examples.rb +43 -0
  31. data/spec/moxml/native_attachment_spec.rb +184 -0
  32. data/spec/moxml/text_spec.rb +23 -0
  33. data/spec/moxml/xpath/functions/node_functions_spec.rb +3 -2
  34. data/spec/performance/benchmark_spec.rb +1 -1
  35. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1daf227e9effc582c66e780516135481aa48e467226a4267974633a4673f786
4
- data.tar.gz: 5b230e79a208eb4b1c5e32175df364e5b42f9e766c4c8189eb18fcad09bb79bf
3
+ metadata.gz: 0633c51783a25d02190769345f0c6a4af79c29665b180e5b46fc28f0b6eeee31
4
+ data.tar.gz: b3d9d706cc185c2d045ed8d3c9a62bb884196cc2cc520cd51fd17cecb07cd897
5
5
  SHA512:
6
- metadata.gz: e39941f6f51567655c246f1e8d6225c6ab572c68958fd9ca4a74ca191af4493e1bf74ff0feed84792cc87e5681cce1d8ae291533e992d07b70fdfb1063d96a67
7
- data.tar.gz: 41ca4a3954bf2e713703124758309e5d1e056d44826cdacf3b4693c5bf37c463579462a6c0ecb3aa65e4c3cafaeef1e2ad0d8a5af9ebe2e1b9f4a695e7678ff4
6
+ metadata.gz: 6626a13b9dda295113caa1e1d99085afd20776b1d3d4f156bbdf63efcefccef59cd1ea37d14439aa7487f3f617ed65ac1003ec775b22e77614a4b0082c832307
7
+ data.tar.gz: 68b26d50fa35b206835f633dfd7b26f6c389fc3507a524a84784929b089e0c699d1361af8838d86e5fcec0e8031f81ca82e2e799b544737cc1616a3da5d15707
data/.rubocop_todo.yml CHANGED
@@ -1,11 +1,65 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-04-23 07:48:23 UTC using RuboCop version 1.86.0.
3
+ # on 2026-05-03 12:53:32 UTC using RuboCop version 1.86.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ # Offense count: 5
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
12
+ # SupportedStyles: with_first_argument, with_fixed_indentation
13
+ Layout/ArgumentAlignment:
14
+ Exclude:
15
+ - 'lib/moxml/adapter/base.rb'
16
+ - 'lib/moxml/adapter/libxml.rb'
17
+ - 'lib/moxml/builder.rb'
18
+
19
+ # Offense count: 2
20
+ # This cop supports safe autocorrection (--autocorrect).
21
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
22
+ # SupportedStyles: with_first_element, with_fixed_indentation
23
+ Layout/ArrayAlignment:
24
+ Exclude:
25
+ - 'lib/moxml/xpath/compiler.rb'
26
+
27
+ # Offense count: 9
28
+ # This cop supports safe autocorrection (--autocorrect).
29
+ # Configuration parameters: EnforcedStyleAlignWith.
30
+ # SupportedStylesAlignWith: either, start_of_block, start_of_line
31
+ Layout/BlockAlignment:
32
+ Exclude:
33
+ - 'lib/moxml/adapter/libxml.rb'
34
+ - 'lib/moxml/adapter/ox.rb'
35
+ - 'spec/integration/shared_examples/edge_cases.rb'
36
+ - 'spec/integration/shared_examples/high_level/document_builder_behavior.rb'
37
+ - 'spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb'
38
+ - 'spec/moxml/xpath/functions/node_functions_spec.rb'
39
+
40
+ # Offense count: 9
41
+ # This cop supports safe autocorrection (--autocorrect).
42
+ Layout/BlockEndNewline:
43
+ Exclude:
44
+ - 'lib/moxml/adapter/libxml.rb'
45
+ - 'lib/moxml/adapter/ox.rb'
46
+ - 'spec/integration/shared_examples/edge_cases.rb'
47
+ - 'spec/integration/shared_examples/high_level/document_builder_behavior.rb'
48
+ - 'spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb'
49
+ - 'spec/moxml/xpath/functions/node_functions_spec.rb'
50
+
51
+ # Offense count: 2
52
+ # This cop supports safe autocorrection (--autocorrect).
53
+ Layout/ClosingParenthesisIndentation:
54
+ Exclude:
55
+ - 'lib/moxml/adapter/oga.rb'
56
+
57
+ # Offense count: 1
58
+ # This cop supports safe autocorrection (--autocorrect).
59
+ Layout/ElseAlignment:
60
+ Exclude:
61
+ - 'lib/moxml/adapter/base.rb'
62
+
9
63
  # Offense count: 4
10
64
  # This cop supports safe autocorrection (--autocorrect).
11
65
  Layout/EmptyLineAfterGuardClause:
@@ -27,13 +81,67 @@ Layout/EmptyLines:
27
81
  Exclude:
28
82
  - 'lib/moxml/adapter/ox.rb'
29
83
 
30
- # Offense count: 330
84
+ # Offense count: 2
85
+ # This cop supports safe autocorrection (--autocorrect).
86
+ Layout/EmptyLinesAroundMethodBody:
87
+ Exclude:
88
+ - 'lib/moxml/adapter/ox.rb'
89
+
90
+ # Offense count: 1
91
+ # This cop supports safe autocorrection (--autocorrect).
92
+ # Configuration parameters: EnforcedStyleAlignWith.
93
+ # SupportedStylesAlignWith: keyword, variable, start_of_line
94
+ Layout/EndAlignment:
95
+ Exclude:
96
+ - 'lib/moxml/adapter/base.rb'
97
+
98
+ # Offense count: 2
99
+ # This cop supports safe autocorrection (--autocorrect).
100
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
101
+ # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
102
+ Layout/FirstArgumentIndentation:
103
+ Exclude:
104
+ - 'lib/moxml/adapter/oga.rb'
105
+
106
+ # Offense count: 2
107
+ # This cop supports safe autocorrection (--autocorrect).
108
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
109
+ # SupportedHashRocketStyles: key, separator, table
110
+ # SupportedColonStyles: key, separator, table
111
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
112
+ Layout/HashAlignment:
113
+ Exclude:
114
+ - 'lib/moxml/builder.rb'
115
+
116
+ # Offense count: 20
117
+ # This cop supports safe autocorrection (--autocorrect).
118
+ # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
119
+ # SupportedStylesAlignWith: start_of_line, relative_to_receiver
120
+ Layout/IndentationWidth:
121
+ Exclude:
122
+ - 'lib/moxml/adapter/base.rb'
123
+ - 'lib/moxml/adapter/libxml.rb'
124
+ - 'lib/moxml/adapter/ox.rb'
125
+ - 'spec/integration/shared_examples/edge_cases.rb'
126
+ - 'spec/integration/shared_examples/high_level/document_builder_behavior.rb'
127
+ - 'spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb'
128
+ - 'spec/moxml/xpath/functions/node_functions_spec.rb'
129
+
130
+ # Offense count: 344
31
131
  # This cop supports safe autocorrection (--autocorrect).
32
132
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
33
133
  # URISchemes: http, https
34
134
  Layout/LineLength:
35
135
  Enabled: false
36
136
 
137
+ # Offense count: 2
138
+ # This cop supports safe autocorrection (--autocorrect).
139
+ # Configuration parameters: EnforcedStyle.
140
+ # SupportedStyles: symmetrical, new_line, same_line
141
+ Layout/MultilineMethodCallBraceLayout:
142
+ Exclude:
143
+ - 'lib/moxml/adapter/oga.rb'
144
+
37
145
  # Offense count: 1
38
146
  # This cop supports safe autocorrection (--autocorrect).
39
147
  # Configuration parameters: EnforcedStyle, IndentationWidth.
@@ -42,6 +150,17 @@ Layout/MultilineOperationIndentation:
42
150
  Exclude:
43
151
  - 'lib/moxml/adapter/ox.rb'
44
152
 
153
+ # Offense count: 10
154
+ # This cop supports safe autocorrection (--autocorrect).
155
+ # Configuration parameters: AllowInHeredoc.
156
+ Layout/TrailingWhitespace:
157
+ Exclude:
158
+ - 'lib/moxml/adapter/base.rb'
159
+ - 'lib/moxml/adapter/libxml.rb'
160
+ - 'lib/moxml/adapter/ox.rb'
161
+ - 'lib/moxml/builder.rb'
162
+ - 'lib/moxml/xpath/compiler.rb'
163
+
45
164
  # Offense count: 7
46
165
  # Configuration parameters: AllowedMethods.
47
166
  # AllowedMethods: enums
@@ -61,11 +180,10 @@ Lint/DuplicateBranch:
61
180
  - 'lib/moxml/document.rb'
62
181
  - 'lib/moxml/entity_registry.rb'
63
182
 
64
- # Offense count: 5
183
+ # Offense count: 4
65
184
  Lint/DuplicateMethods:
66
185
  Exclude:
67
186
  - 'lib/moxml/config.rb'
68
- - 'lib/moxml/element.rb'
69
187
  - 'lib/moxml/node.rb'
70
188
 
71
189
  # Offense count: 4
@@ -91,7 +209,7 @@ Lint/EmptyWhen:
91
209
  # Offense count: 3
92
210
  Lint/HashCompareByIdentity:
93
211
  Exclude:
94
- - 'lib/moxml/native_attachment.rb'
212
+ - 'lib/moxml/native_attachment/native.rb'
95
213
 
96
214
  # Offense count: 1
97
215
  Lint/IneffectiveAccessModifier:
@@ -127,12 +245,12 @@ Metrics/BlockLength:
127
245
  Metrics/BlockNesting:
128
246
  Max: 4
129
247
 
130
- # Offense count: 76
248
+ # Offense count: 75
131
249
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
132
250
  Metrics/CyclomaticComplexity:
133
251
  Enabled: false
134
252
 
135
- # Offense count: 186
253
+ # Offense count: 188
136
254
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
137
255
  Metrics/MethodLength:
138
256
  Max: 110
@@ -180,6 +298,12 @@ Naming/VariableNumber:
180
298
  - 'spec/moxml/allocation_guard_spec.rb'
181
299
  - 'spec/support/allocation_helper.rb'
182
300
 
301
+ # Offense count: 1
302
+ # Configuration parameters: MinSize.
303
+ Performance/CollectionLiteralInLoop:
304
+ Exclude:
305
+ - 'lib/moxml/xpath/compiler.rb'
306
+
183
307
  # Offense count: 5
184
308
  RSpec/BeforeAfterAll:
185
309
  Exclude:
@@ -205,12 +329,12 @@ RSpec/ContextWording:
205
329
  - 'spec/moxml/xpath/parser_spec.rb'
206
330
  - 'spec/performance/benchmark_spec.rb'
207
331
 
208
- # Offense count: 24
332
+ # Offense count: 25
209
333
  # Configuration parameters: IgnoredMetadata.
210
334
  RSpec/DescribeClass:
211
335
  Enabled: false
212
336
 
213
- # Offense count: 295
337
+ # Offense count: 328
214
338
  # Configuration parameters: CountAsOne.
215
339
  RSpec/ExampleLength:
216
340
  Max: 64
@@ -240,13 +364,13 @@ RSpec/LeakyConstantDeclaration:
240
364
  - 'spec/moxml/declaration_preservation_spec.rb'
241
365
  - 'spec/moxml/sax_spec.rb'
242
366
 
243
- # Offense count: 2
367
+ # Offense count: 4
244
368
  # Configuration parameters: .
245
369
  # SupportedStyles: have_received, receive
246
370
  RSpec/MessageSpies:
247
371
  EnforcedStyle: receive
248
372
 
249
- # Offense count: 390
373
+ # Offense count: 414
250
374
  RSpec/MultipleExpectations:
251
375
  Max: 10
252
376
 
@@ -306,6 +430,22 @@ Security/Eval:
306
430
  Exclude:
307
431
  - 'spec/moxml/xpath/ruby/generator_spec.rb'
308
432
 
433
+ # Offense count: 12
434
+ # This cop supports safe autocorrection (--autocorrect).
435
+ # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
436
+ # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
437
+ # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
438
+ # FunctionalMethods: let, let!, subject, watch
439
+ # AllowedMethods: lambda, proc, it
440
+ Style/BlockDelimiters:
441
+ Exclude:
442
+ - 'lib/moxml/adapter/libxml.rb'
443
+ - 'lib/moxml/adapter/ox.rb'
444
+ - 'spec/integration/shared_examples/edge_cases.rb'
445
+ - 'spec/integration/shared_examples/high_level/document_builder_behavior.rb'
446
+ - 'spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb'
447
+ - 'spec/moxml/xpath/functions/node_functions_spec.rb'
448
+
309
449
  # Offense count: 1
310
450
  Style/DocumentDynamicEvalDefinition:
311
451
  Exclude:
@@ -329,6 +469,18 @@ Style/MissingRespondToMissing:
329
469
  Exclude:
330
470
  - 'lib/moxml/xpath/ruby/node.rb'
331
471
 
472
+ # Offense count: 1
473
+ # This cop supports safe autocorrection (--autocorrect).
474
+ Style/MultilineIfModifier:
475
+ Exclude:
476
+ - 'lib/moxml/builder.rb'
477
+
478
+ # Offense count: 1
479
+ # This cop supports safe autocorrection (--autocorrect).
480
+ Style/MultilineTernaryOperator:
481
+ Exclude:
482
+ - 'lib/moxml/adapter/base.rb'
483
+
332
484
  # Offense count: 1
333
485
  # This cop supports safe autocorrection (--autocorrect).
334
486
  # Configuration parameters: AllowMethodComparison, ComparisonsThreshold.
@@ -365,7 +517,25 @@ Style/RedundantConstantBase:
365
517
  - 'spec/moxml/adapter/headed_ox_spec.rb'
366
518
 
367
519
  # Offense count: 1
520
+ # This cop supports safe autocorrection (--autocorrect).
521
+ Style/RedundantParentheses:
522
+ Exclude:
523
+ - 'lib/moxml/xpath/compiler.rb'
524
+
525
+ # Offense count: 8
368
526
  # This cop supports unsafe autocorrection (--autocorrect-all).
369
527
  Style/SelectByKind:
370
528
  Exclude:
529
+ - 'spec/integration/shared_examples/edge_cases.rb'
530
+ - 'spec/integration/shared_examples/entity_reference_whitespace.rb'
371
531
  - 'spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb'
532
+ - 'spec/integration/shared_examples/node_wrappers/node_set_behavior.rb'
533
+ - 'spec/moxml/xpath/functions/node_functions_spec.rb'
534
+
535
+ # Offense count: 2
536
+ # This cop supports safe autocorrection (--autocorrect).
537
+ # Configuration parameters: EnforcedStyle, MinSize.
538
+ # SupportedStyles: percent, brackets
539
+ Style/SymbolArray:
540
+ Exclude:
541
+ - 'lib/moxml/xpath/compiler.rb'
data/README.adoc CHANGED
@@ -33,7 +33,7 @@ Moxml supports the following XML libraries:
33
33
  REXML:: https://github.com/ruby/rexml[REXML], a pure Ruby XML parser
34
34
  distributed with standard Ruby. Not the fastest, but always available.
35
35
 
36
- Nokogiri:: (default) https://github.com/sparklemotion/nokogiri[Nokogiri], a
36
+ Nokogiri:: https://github.com/sparklemotion/nokogiri[Nokogiri], a
37
37
  widely used implementation which wraps around the performant
38
38
  https://github.com/GNOME/libxml2[libxml2] C library.
39
39
 
@@ -47,6 +47,29 @@ LibXML:: https://github.com/xml4r/libxml-ruby[libxml-ruby], Ruby bindings
47
47
  for the performant https://github.com/GNOME/libxml2[libxml2] C library.
48
48
  Alternative to Nokogiri with similar performance characteristics.
49
49
 
50
+ ==== Default adapter selection
51
+
52
+ When no adapter is explicitly specified, Moxml selects one automatically based on
53
+ the runtime environment:
54
+
55
+ . If running on Opal (`RUBY_ENGINE == "opal"`), Oga is used (pure Ruby, no C extensions)
56
+ . Otherwise, Moxml detects already-loaded XML libraries in this order:
57
+ .. Nokogiri
58
+ .. Ox
59
+ .. Oga
60
+ . If none of the above are loaded, Nokogiri is the fallback default
61
+
62
+ [source,ruby]
63
+ ----
64
+ # Automatic detection — picks the best available adapter
65
+ ctx = Moxml.new
66
+ ctx.config.adapter_name # => :nokogiri (or :oga on Opal)
67
+
68
+ # Explicit override — always takes precedence
69
+ ctx = Moxml.new(:ox)
70
+ ctx.config.adapter_name # => :ox
71
+ ----
72
+
50
73
  === Feature table
51
74
 
52
75
  Moxml exercises its best effort to provide a consistent interface across basic
@@ -390,6 +390,10 @@ Text nodes contain character data.
390
390
  | `#text`
391
391
  | Alias for #content
392
392
  | ✅ Yes
393
+
394
+ | `#to_s`
395
+ | Returns the text content (same as `#content`)
396
+ | ✅ Yes
393
397
  |===
394
398
 
395
399
  ==== Identity Methods
@@ -35,7 +35,12 @@ module Moxml
35
35
  # not valid UTF-8, fall back to encoding as UTF-8 with
36
36
  # replacement to avoid raising on gsub.
37
37
  dup = xml.dup.force_encoding("UTF-8")
38
- dup.valid_encoding? ? dup : xml.dup.encode("UTF-8", "ASCII-8BIT", invalid: :replace, undef: :replace)
38
+ if dup.valid_encoding?
39
+ dup
40
+ else
41
+ xml.dup.encode("UTF-8",
42
+ "ASCII-8BIT", invalid: :replace, undef: :replace)
43
+ end
39
44
  elsif xml.encoding == Encoding::UTF_8
40
45
  xml
41
46
  else
@@ -3,8 +3,21 @@
3
3
  module Moxml
4
4
  module Adapter
5
5
  module CustomizedOx
6
- # Ox uses Strings, but a string cannot have a parent reference
7
- class Text < ::Ox::Node; end
6
+ # Ox uses Strings for text content, but a String cannot carry a @parent
7
+ # back-reference. We subclass ::Ox::Node so a Text wrapper can hold one.
8
+ #
9
+ # ::Ox::Node subclasses that are neither ::Ox::Element nor ::Ox::Document
10
+ # are unknown to Ox.dump's standard XML emitter, so they fall through to
11
+ # Ox's generic object-marshalling format. The serializer in
12
+ # Moxml::Adapter::Ox#serialize special-cases this class to emit the value
13
+ # with proper XML escaping. The #to_s override ensures string
14
+ # interpolation (`"#{text}"`) produces the text content rather than the
15
+ # default Object representation.
16
+ class Text < ::Ox::Node
17
+ def to_s
18
+ value.to_s
19
+ end
20
+ end
8
21
  end
9
22
  end
10
23
  end
@@ -68,6 +68,7 @@ module Moxml
68
68
  next if child.is_a?(::REXML::Text) &&
69
69
  child.to_s.strip.empty? &&
70
70
  !(child.next_sibling.nil? && child.previous_sibling.nil?)
71
+
71
72
  write(child, output)
72
73
  end
73
74
  when :eref
@@ -565,7 +565,8 @@ module Moxml
565
565
  # Set as root element
566
566
  native_elem.root = native_child
567
567
  # Flag for actual_native to refresh the wrapper's native reference
568
- attachments.set(native_elem, :_pending_root_refresh, native_child.object_id)
568
+ attachments.set(native_elem, :_pending_root_refresh,
569
+ native_child.object_id)
569
570
  elsif native_elem.root
570
571
  # Document has root, add to it instead
571
572
  import_and_add(native_elem.doc, native_elem.root, native_child)
@@ -594,6 +595,7 @@ module Moxml
594
595
  def lookup_entity_refs(doc, element)
595
596
  pairs = attachments.get(doc, :_entity_ref_pairs)
596
597
  return nil unless pairs
598
+
597
599
  pair = pairs.find { |elem, _| elem == element }
598
600
  pair&.last
599
601
  end
@@ -614,6 +616,7 @@ module Moxml
614
616
  def lookup_child_sequence(doc, element)
615
617
  pairs = attachments.get(doc, :_child_seq_pairs)
616
618
  return nil unless pairs
619
+
617
620
  pair = pairs.find { |elem, _| elem == element }
618
621
  pair&.last
619
622
  end
@@ -1481,7 +1484,9 @@ module Moxml
1481
1484
  # Interleave native children with entity refs using tracked sequence
1482
1485
  native_children = []
1483
1486
  if elem.children?
1484
- elem.each_child { |c| native_children << c unless c.text? && c.content.to_s.strip.empty? }
1487
+ elem.each_child do |c|
1488
+ native_children << c unless c.text? && c.content.to_s.strip.empty?
1489
+ end
1485
1490
  end
1486
1491
 
1487
1492
  eref_idx = 0
@@ -470,7 +470,9 @@ module Moxml
470
470
  if node.is_a?(::Oga::XML::Document)
471
471
  # Check if we should include declaration
472
472
  # Priority: explicit option > existence of xml_declaration (native or attachment)
473
- effective_xml_declaration = node.xml_declaration || attachments.get(node, :xml_declaration)
473
+ effective_xml_declaration = node.xml_declaration || attachments.get(
474
+ node, :xml_declaration
475
+ )
474
476
  should_include_decl = if options.key?(:no_declaration)
475
477
  !options[:no_declaration]
476
478
  elsif options.key?(:declaration)
@@ -519,7 +521,9 @@ module Moxml
519
521
 
520
522
  # Default: use XmlGenerator
521
523
  # But first check if we need to handle declaration specially
522
- effective_xml_declaration = node.is_a?(::Oga::XML::Document) && (node.xml_declaration || attachments.get(node, :xml_declaration))
524
+ effective_xml_declaration = node.is_a?(::Oga::XML::Document) && (node.xml_declaration || attachments.get(
525
+ node, :xml_declaration
526
+ ))
523
527
  if node.is_a?(::Oga::XML::Document) && effective_xml_declaration
524
528
  # Document has declaration - use custom handling to avoid duplicates
525
529
  output = []
@@ -74,8 +74,7 @@ module Moxml
74
74
  end
75
75
 
76
76
  def create_native_element(name, _owner_doc = nil)
77
- element = ::Ox::Element.new(name)
78
- element
77
+ ::Ox::Element.new(name)
79
78
  end
80
79
 
81
80
  def create_native_text(content, _owner_doc = nil)
@@ -622,6 +621,12 @@ module Moxml
622
621
  end
623
622
 
624
623
  def serialize(node, options = {})
624
+ # CustomizedOx::Text subclasses ::Ox::Node so it can carry a @parent
625
+ # back-reference, but that makes it unknown to Ox.dump's XML emitter,
626
+ # which then falls back to generic object marshalling. Short-circuit
627
+ # here with proper XML escaping.
628
+ return escape_xml_text(node.value) if node.is_a?(CustomizedOx::Text)
629
+
625
630
  needs_custom = needs_custom_serialize?(node)
626
631
 
627
632
  unless needs_custom
@@ -643,7 +648,7 @@ module Moxml
643
648
  return true if attachments.get(node, :has_entity_refs)
644
649
  return true if attachments.get(node, :has_cdata_end_markers)
645
650
  return false if attachments.key?(node, :has_entity_refs) &&
646
- attachments.key?(node, :has_cdata_end_markers)
651
+ attachments.key?(node, :has_cdata_end_markers)
647
652
  end
648
653
 
649
654
  # Only scan tree on first call — short-circuit on first hit
@@ -694,9 +699,8 @@ module Moxml
694
699
  encoding: options[:encoding],
695
700
  no_empty: options[:expand_empty],
696
701
  }
697
- result = output + ::Ox.dump(node, ox_options)
702
+ output + ::Ox.dump(node, ox_options)
698
703
  # Fix CDATA ]]> end markers that Ox doesn't escape
699
- result
700
704
  end
701
705
 
702
706
  def tree_has_entity_references?(node)
@@ -721,9 +725,13 @@ module Moxml
721
725
  when ::Ox::CData
722
726
  node.value&.include?("]]>") || false
723
727
  when ::Ox::Element
724
- node.nodes&.any? { |child| tree_has_cdata_end_markers?(child) } || false
728
+ node.nodes&.any? do |child|
729
+ tree_has_cdata_end_markers?(child)
730
+ end || false
725
731
  when ::Ox::Document
726
- node.nodes&.any? { |child| tree_has_cdata_end_markers?(child) } || false
732
+ node.nodes&.any? do |child|
733
+ tree_has_cdata_end_markers?(child)
734
+ end || false
727
735
  else
728
736
  false
729
737
  end
@@ -814,7 +822,6 @@ module Moxml
814
822
  end
815
823
  end
816
824
 
817
-
818
825
  # Translate a subset of XPath to Ox locate() syntax
819
826
  # Supports: //element, /path/to/element, .//element, element[@attr]
820
827
  # Note: Ox locate() doesn't support namespace prefixes in the path
data/lib/moxml/builder.rb CHANGED
@@ -30,12 +30,14 @@ module Moxml
30
30
  # and a valid XML tag name (XSD/RelaxNG).
31
31
  def element(name_or_attrs = nil, attributes = {}, &block)
32
32
  if name_or_attrs.is_a?(Hash)
33
- return create_element_node("element", name_or_attrs, block: block, eval_block: false)
33
+ return create_element_node("element", name_or_attrs, block: block,
34
+ eval_block: false)
34
35
  end
35
36
 
36
37
  raise ArgumentError, "element requires a tag name" if name_or_attrs.nil?
37
38
 
38
- create_element_node(name_or_attrs, attributes, block: block, eval_block: true)
39
+ create_element_node(name_or_attrs, attributes, block: block,
40
+ eval_block: true)
39
41
  end
40
42
 
41
43
  def text(content)
@@ -101,10 +103,14 @@ module Moxml
101
103
  text_content = args.first.is_a?(String) ? args.shift : nil
102
104
  attrs = args.first.is_a?(Hash) ? args.shift : {}
103
105
 
104
- raise ArgumentError, "unexpected arguments for #{method_name}: #{args.inspect}" unless args.empty?
106
+ unless args.empty?
107
+ raise ArgumentError,
108
+ "unexpected arguments for #{method_name}: #{args.inspect}"
109
+ end
105
110
 
106
111
  if text_content && block
107
- raise ArgumentError, "#{method_name}: cannot combine text content with a block"
112
+ raise ArgumentError,
113
+ "#{method_name}: cannot combine text content with a block"
108
114
  end
109
115
 
110
116
  # Strip trailing underscore to allow reserved Ruby method names as tags
@@ -126,7 +132,8 @@ module Moxml
126
132
  # Single method for all element creation.
127
133
  # eval_block: true → instance_eval (build DSL context)
128
134
  # eval_block: false → yield (preserves caller's self)
129
- def create_element_node(tag_name, attrs = {}, text_content: nil, block: nil, eval_block: true)
135
+ def create_element_node(tag_name, attrs = {}, text_content: nil,
136
+ block: nil, eval_block: true)
130
137
  el = @document.create_element(tag_name)
131
138
 
132
139
  attrs.each do |key, value|
data/lib/moxml/config.rb CHANGED
@@ -21,7 +21,7 @@ module Moxml
21
21
  end
22
22
 
23
23
  def default_adapter
24
- @default_adapter ||= runtime_default_adapter
24
+ @default_adapter || runtime_default_adapter
25
25
  end
26
26
 
27
27
  def runtime_default_adapter
@@ -156,6 +156,7 @@ module Moxml
156
156
  def primary_name_for_codepoint(codepoint)
157
157
  names = @by_codepoint[codepoint]
158
158
  return nil unless names&.any?
159
+
159
160
  # Prefer lowercase names (e.g., "amp" over "AMP") for XML compatibility
160
161
  names.find { |n| n == n.downcase } || names.first
161
162
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Moxml
6
+ class NativeAttachment
7
+ # Stores Moxml-specific state associated with native adapter objects
8
+ # without polluting their internals.
9
+ #
10
+ # Uses object_id as key with GC finalizer cleanup to prevent memory leaks.
11
+ # Thread-safe via Monitor (reentrant-safe).
12
+ #
13
+ # Replaces the anti-pattern of using instance_variable_set/get on
14
+ # foreign library objects (Nokogiri, REXML, Oga, Ox, LibXML nodes).
15
+ #
16
+ # @example
17
+ # attachments = NativeAttachment.new
18
+ # attachments.set(native_element, :entity_refs, [])
19
+ # refs = attachments.get(native_element, :entity_refs)
20
+ # attachments.key?(native_element, :doctype) #=> false
21
+ class Native
22
+ def initialize
23
+ @data = {}
24
+ @finalizer_registered = {}
25
+ @monitor = Monitor.new
26
+ end
27
+
28
+ def get(native, key)
29
+ @monitor.synchronize { @data[native.object_id]&.[](key) }
30
+ end
31
+
32
+ def set(native, key, value)
33
+ id = native.object_id
34
+ @monitor.synchronize do
35
+ @data[id] ||= {}
36
+ @data[id][key] = value
37
+ register_finalizer(native, id) unless @finalizer_registered[id]
38
+ end
39
+ end
40
+
41
+ def key?(native, key)
42
+ @monitor.synchronize { @data[native.object_id]&.key?(key) || false }
43
+ end
44
+
45
+ def delete(native, key)
46
+ @monitor.synchronize { @data[native.object_id]&.delete(key) }
47
+ end
48
+
49
+ private
50
+
51
+ def register_finalizer(native, id)
52
+ @finalizer_registered[id] = true
53
+ ObjectSpace.define_finalizer(native, finalizer_for(id))
54
+ end
55
+
56
+ def finalizer_for(id)
57
+ data = @data
58
+ registered = @finalizer_registered
59
+ # Finalizers must NOT use Mutex/Monitor (can't be called from trap context).
60
+ # Direct Hash operations are safe here since finalizers run sequentially
61
+ # and the GC'd object's id won't be accessed by any other thread.
62
+ proc do
63
+ data.delete(id)
64
+ registered.delete(id)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end