moxml 0.1.22 → 0.1.24

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -0
  4. data/.rubocop_todo.yml +680 -110
  5. data/Rakefile +12 -9
  6. data/lib/compat/opal/moxml_boot.rb +53 -0
  7. data/lib/compat/opal/rexml/namespace.rb +8 -5
  8. data/lib/compat/opal/rexml/parsers/baseparser.rb +276 -212
  9. data/lib/compat/opal/rexml/source.rb +28 -27
  10. data/lib/compat/opal/rexml/text.rb +112 -104
  11. data/lib/compat/opal/rexml/xmltokens.rb +8 -8
  12. data/lib/compat/opal/rexml_compat.rb +12 -11
  13. data/lib/moxml/adapter/base.rb +5 -5
  14. data/lib/moxml/adapter/customized_libxml/cdata.rb +0 -2
  15. data/lib/moxml/adapter/customized_libxml/comment.rb +0 -2
  16. data/lib/moxml/adapter/customized_libxml/element.rb +3 -5
  17. data/lib/moxml/adapter/customized_libxml/node.rb +2 -2
  18. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +0 -2
  19. data/lib/moxml/adapter/customized_libxml/text.rb +0 -2
  20. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +8 -1
  21. data/lib/moxml/adapter/customized_rexml/formatter.rb +7 -8
  22. data/lib/moxml/adapter/headed_ox.rb +1 -5
  23. data/lib/moxml/adapter/libxml/entity_ref_registry.rb +4 -2
  24. data/lib/moxml/adapter/libxml/entity_restorer.rb +3 -1
  25. data/lib/moxml/adapter/libxml.rb +29 -14
  26. data/lib/moxml/adapter/nokogiri.rb +21 -17
  27. data/lib/moxml/adapter/oga.rb +48 -67
  28. data/lib/moxml/adapter/ox.rb +61 -31
  29. data/lib/moxml/adapter/rexml.rb +5 -6
  30. data/lib/moxml/adapter.rb +13 -5
  31. data/lib/moxml/attribute.rb +5 -2
  32. data/lib/moxml/config.rb +16 -2
  33. data/lib/moxml/context.rb +8 -8
  34. data/lib/moxml/declaration.rb +2 -7
  35. data/lib/moxml/document.rb +2 -19
  36. data/lib/moxml/document_builder.rb +12 -8
  37. data/lib/moxml/element.rb +4 -4
  38. data/lib/moxml/entity_registry.rb +8 -4
  39. data/lib/moxml/entity_registry_opal_data.rb +3 -2
  40. data/lib/moxml/node.rb +45 -14
  41. data/lib/moxml/node_set.rb +2 -4
  42. data/lib/moxml/sax/block_handler.rb +0 -2
  43. data/lib/moxml/sax/element_handler.rb +0 -2
  44. data/lib/moxml/sax/namespace_splitter.rb +5 -4
  45. data/lib/moxml/sax.rb +4 -25
  46. data/lib/moxml/version.rb +1 -1
  47. data/lib/moxml/xml_utils.rb +2 -3
  48. data/lib/moxml/xpath/compiler.rb +1 -49
  49. data/lib/moxml/xpath/conversion.rb +7 -6
  50. data/lib/moxml/xpath/ruby/generator.rb +12 -19
  51. data/lib/moxml/xpath/ruby/node.rb +1 -9
  52. data/lib/moxml/xpath.rb +6 -14
  53. data/lib/moxml.rb +74 -20
  54. data/spec/integration/all_adapters_spec.rb +1 -0
  55. data/spec/integration/shared_examples/line_ending_behavior.rb +56 -0
  56. data/spec/moxml/adapter/libxml_internals_spec.rb +4 -2
  57. data/spec/moxml/adapter/platform_spec.rb +2 -1
  58. data/spec/moxml/attribute_spec.rb +16 -0
  59. data/spec/moxml/config_spec.rb +33 -0
  60. data/spec/moxml/context_spec.rb +14 -0
  61. data/spec/moxml/moxml_spec.rb +13 -0
  62. data/spec/moxml/node_spec.rb +58 -0
  63. data/spec/moxml/xpath/ruby/node_spec.rb +3 -3
  64. metadata +4 -2
@@ -2,11 +2,8 @@
2
2
 
3
3
  return if RUBY_ENGINE == "opal"
4
4
 
5
- require_relative "base"
6
5
  require "ox"
7
6
  require "stringio"
8
- require_relative "customized_ox"
9
- require_relative "../sax/namespace_splitter"
10
7
 
11
8
  # insert :parent methods to all Ox classes inherit the Node class
12
9
  Ox::Node.attr_accessor :parent
@@ -19,7 +16,16 @@ module Moxml
19
16
  end
20
17
 
21
18
  def set_root(doc, element)
22
- replace_children(doc, [element])
19
+ existing_root = root(doc)
20
+ element.parent = doc if element.is_a?(::Ox::Node)
21
+ if existing_root
22
+ # Replace the existing root element, preserving other children
23
+ idx = doc.nodes.index(existing_root)
24
+ doc.nodes[idx] = element
25
+ else
26
+ # No root yet, just append the element
27
+ doc << element
28
+ end
23
29
  end
24
30
 
25
31
  def parse(xml, options = {}, _context = nil)
@@ -101,9 +107,14 @@ module Moxml
101
107
  end
102
108
 
103
109
  def create_native_doctype(name, external_id, system_id)
104
- ::Ox::DocType.new(
105
- "#{name} PUBLIC \"#{external_id}\" \"#{system_id}\"",
106
- )
110
+ value = if external_id
111
+ "#{name} PUBLIC \"#{external_id}\" \"#{system_id}\""
112
+ elsif system_id
113
+ "#{name} SYSTEM \"#{system_id}\""
114
+ else
115
+ name.to_s
116
+ end
117
+ ::Ox::DocType.new(value)
107
118
  end
108
119
 
109
120
  def create_native_processing_instruction(target, content)
@@ -257,7 +268,7 @@ module Moxml
257
268
  # Ox doesn't set parent references during parsing.
258
269
  # Set them here so parent/sibling navigation works.
259
270
  result.each do |child|
260
- child.parent = node if child.respond_to?(:parent=)
271
+ child.parent = node if child.is_a?(::Ox::Element)
261
272
  end
262
273
  result
263
274
  end
@@ -368,27 +379,32 @@ module Moxml
368
379
  end
369
380
 
370
381
  def add_child(element, child)
371
- # Special handling for declarations on Ox documents
372
382
  if element.is_a?(::Ox::Document) && child.is_a?(::Ox::Instruct) && child.target == "xml"
373
- # Transfer declaration attributes to document
374
383
  element.attributes ||= {}
375
- if child.attributes["version"]
376
- element.attributes[:version] =
377
- child.attributes["version"]
378
- end
379
- if child.attributes["encoding"]
380
- element.attributes[:encoding] =
381
- child.attributes["encoding"]
382
- end
383
- if child.attributes["standalone"]
384
- element.attributes[:standalone] =
385
- child.attributes["standalone"]
386
- end
384
+ element.attributes[:version] = child.attributes["version"] if child.attributes["version"]
385
+ element.attributes[:encoding] = child.attributes["encoding"] if child.attributes["encoding"]
386
+ element.attributes[:standalone] = child.attributes["standalone"] if child.attributes["standalone"]
387
+ attachments.set(element, :decl_explicit, {
388
+ encoding: child.attributes.key?("encoding") ? child.attributes["encoding"] : nil,
389
+ standalone: child.attributes.key?("standalone") ? child.attributes["standalone"] : nil,
390
+ })
391
+ return
387
392
  end
388
393
 
389
394
  child.parent = element if child.is_a?(::Ox::Node)
390
395
  element.nodes ||= []
391
- element.nodes << child
396
+
397
+ # Insert doctype before root element in document
398
+ if element.is_a?(::Ox::Document) && child.is_a?(::Ox::DocType)
399
+ root_idx = element.nodes.index { |n| n.is_a?(::Ox::Element) }
400
+ if root_idx
401
+ element.nodes.insert(root_idx, child)
402
+ else
403
+ element.nodes << child
404
+ end
405
+ else
406
+ element.nodes << child
407
+ end
392
408
 
393
409
  # Mark document if EntityReference is added (avoids tree scan in serialize)
394
410
  if child.is_a?(::Moxml::Adapter::CustomizedOx::EntityReference)
@@ -464,8 +480,8 @@ module Moxml
464
480
  end
465
481
 
466
482
  def assign_parents(node, parent = nil)
467
- node.parent = parent if node.respond_to?(:parent=) && parent
468
- return unless node.respond_to?(:nodes)
483
+ node.parent = parent if node.is_a?(::Ox::Element) && parent
484
+ return unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
469
485
 
470
486
  node.nodes&.each do |child|
471
487
  assign_parents(child, node)
@@ -671,12 +687,26 @@ module Moxml
671
687
  end
672
688
 
673
689
  def has_declaration?(native_doc, _wrapper)
674
- # Ox stores declaration in document attributes
675
690
  native_doc[:version] || native_doc[:encoding] || native_doc[:standalone]
676
691
  end
677
692
 
693
+ def remove_declaration(native_doc)
694
+ native_doc.attributes&.delete(:version)
695
+ native_doc.attributes&.delete(:encoding)
696
+ native_doc.attributes&.delete(:standalone)
697
+ attachments.delete(native_doc, :decl_explicit)
698
+ end
699
+
678
700
  private
679
701
 
702
+ def resolve_decl_attr(node, attr, fallback)
703
+ if attachments.key?(node, :decl_explicit)
704
+ attachments.get(node, :decl_explicit)[attr]
705
+ else
706
+ node[attr] || fallback
707
+ end
708
+ end
709
+
680
710
  def serialize_standard(node, options = {})
681
711
  output = ""
682
712
  if node.is_a?(::Ox::Document)
@@ -688,8 +718,8 @@ module Moxml
688
718
 
689
719
  if should_include_decl
690
720
  version = node[:version] || "1.0"
691
- encoding = options[:encoding] || node[:encoding]
692
- standalone = node[:standalone]
721
+ encoding = resolve_decl_attr(node, :encoding, options[:encoding])
722
+ standalone = resolve_decl_attr(node, :standalone, nil)
693
723
 
694
724
  decl = create_native_declaration(version, encoding, standalone)
695
725
  output = ::Ox.dump(::Ox::Document.new << decl).strip
@@ -698,7 +728,7 @@ module Moxml
698
728
 
699
729
  ox_options = {
700
730
  indent: -1,
701
- with_instructions: true,
731
+ with_instructions: false,
702
732
  encoding: options[:encoding],
703
733
  no_empty: options[:expand_empty],
704
734
  }
@@ -750,8 +780,8 @@ module Moxml
750
780
  end
751
781
  if should_include_decl
752
782
  version = node[:version] || "1.0"
753
- encoding = options[:encoding] || node[:encoding]
754
- standalone = node[:standalone]
783
+ encoding = resolve_decl_attr(node, :encoding, options[:encoding])
784
+ standalone = resolve_decl_attr(node, :standalone, nil)
755
785
  output << "<?xml version=\"#{version}\""
756
786
  output << " encoding=\"#{encoding}\"" if encoding
757
787
  output << " standalone=\"#{standalone}\"" if standalone
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
3
  require "rexml/document"
5
4
  require "rexml/xpath"
6
5
  require "set" unless RUBY_ENGINE == "opal"
7
6
  require "stringio" if RUBY_ENGINE == "opal"
8
- require_relative "customized_rexml"
9
- require_relative "../sax/namespace_splitter"
10
7
 
11
8
  module Moxml
12
9
  module Adapter
@@ -173,7 +170,7 @@ module Moxml
173
170
  end
174
171
 
175
172
  def duplicate_node(node)
176
- if node.respond_to?(:deep_clone)
173
+ if node.is_a?(::REXML::Parent)
177
174
  node.deep_clone
178
175
  else
179
176
  Marshal.load(Marshal.dump(node))
@@ -620,9 +617,7 @@ module Moxml
620
617
  def has_declaration?(native_doc, wrapper)
621
618
  xml_decl = attachments.get(native_doc, :xml_declaration)
622
619
  if xml_decl.nil?
623
- # Attachment key doesn't exist - check native doc or wrapper flag
624
620
  if attachments.key?(native_doc, :xml_declaration)
625
- # Explicitly set to nil (was removed)
626
621
  false
627
622
  else
628
623
  wrapper.has_xml_declaration
@@ -632,6 +627,10 @@ module Moxml
632
627
  end
633
628
  end
634
629
 
630
+ def remove_declaration(native_doc)
631
+ attachments.set(native_doc, :xml_declaration, nil)
632
+ end
633
+
635
634
  private
636
635
 
637
636
  def write_with_formatter(node, output, indent = 2)
data/lib/moxml/adapter.rb CHANGED
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "adapter/base"
4
-
5
3
  module Moxml
6
4
  module Adapter
5
+ autoload :Base, "moxml/adapter/base"
6
+ autoload :CustomizedOga, "moxml/adapter/customized_oga"
7
+ autoload :CustomizedOx, "moxml/adapter/customized_ox"
8
+ autoload :CustomizedRexml, "moxml/adapter/customized_rexml"
9
+ autoload :CustomizedLibxml, "moxml/adapter/customized_libxml"
10
+
7
11
  AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
8
12
 
9
13
  # Adapters that work under the Opal (JavaScript) runtime.
@@ -46,7 +50,7 @@ module Moxml
46
50
  def validate_platform!(name)
47
51
  return if platform_adapters.include?(name.to_sym)
48
52
 
49
- available = platform_adapters.map(&:to_s).join(", ")
53
+ available = platform_adapters.join(", ")
50
54
  raise Moxml::AdapterError.new(
51
55
  "The '#{name}' adapter is not available on this platform. Available: #{available}",
52
56
  adapter: name,
@@ -59,11 +63,15 @@ module Moxml
59
63
  end
60
64
 
61
65
  def require_adapter(name)
62
- require "#{__dir__}/adapter/#{name}"
66
+ # Opal pre-loads all dependencies via the Rakefile; skip runtime require.
67
+ return if RUBY_ENGINE == "opal"
68
+
69
+ require "moxml/adapter/base"
70
+ require "moxml/adapter/#{name}"
63
71
  rescue LoadError
64
72
  begin
65
73
  require name.to_s
66
- require "#{__dir__}/adapter/#{name}"
74
+ require "moxml/adapter/#{name}"
67
75
  rescue LoadError => e
68
76
  raise Moxml::AdapterError.new(
69
77
  "Failed to load #{name} adapter",
@@ -21,6 +21,8 @@ module Moxml
21
21
  adapter.restore_entities(val)
22
22
  end
23
23
 
24
+ alias content value
25
+
24
26
  # Returns raw native value without entity marker restoration.
25
27
  def raw_value
26
28
  @native.value
@@ -46,11 +48,12 @@ module Moxml
46
48
  end
47
49
 
48
50
  def element
49
- adapter.attribute_element(@native)
51
+ native_elem = adapter.attribute_element(@native)
52
+ native_elem && Moxml::Node.wrap(native_elem, context)
50
53
  end
51
54
 
52
55
  def remove
53
- adapter.remove_attribute(element, name)
56
+ adapter.remove_attribute(adapter.attribute_element(@native), name)
54
57
  if @parent_node.is_a?(Moxml::Element)
55
58
  @parent_node.invalidate_attribute_cache!
56
59
  end
data/lib/moxml/config.rb CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Moxml
4
4
  class Config
5
+ LINE_ENDING_LF = "\n"
6
+ LINE_ENDING_CRLF = "\r\n"
7
+ VALID_LINE_ENDINGS = [LINE_ENDING_LF, LINE_ENDING_CRLF].freeze
5
8
  VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
6
9
  DEFAULT_ADAPTER = :nokogiri
7
10
  OPAL_DEFAULT_ADAPTER = :rexml
@@ -46,7 +49,7 @@ module Moxml
46
49
  # - :strict — only restore DTD-declared entities (falls back to lenient until DTD parsing is implemented)
47
50
  ENTITY_RESTORATION_MODES = %i[strict lenient].freeze
48
51
 
49
- attr_reader :adapter_name
52
+ attr_reader :adapter_name, :default_line_ending
50
53
  attr_accessor :strict_parsing,
51
54
  :default_encoding,
52
55
  :entity_encoding,
@@ -58,13 +61,23 @@ module Moxml
58
61
  :namespace_validation_mode,
59
62
  :entity_restoration_mode
60
63
 
64
+ def default_line_ending=(value)
65
+ unless VALID_LINE_ENDINGS.include?(value)
66
+ raise ArgumentError,
67
+ "Invalid line_ending: #{value.inspect}. " \
68
+ "Must be Config::LINE_ENDING_LF or Config::LINE_ENDING_CRLF"
69
+ end
70
+
71
+ @default_line_ending = value
72
+ end
73
+
61
74
  def initialize(adapter_name = nil, strict_parsing = nil,
62
75
  default_encoding = nil)
63
76
  self.adapter = adapter_name || Config.default.adapter_name
64
77
  @strict_parsing = strict_parsing || Config.default.strict_parsing
65
78
  @default_encoding = default_encoding || Config.default.default_encoding
66
- # reserved for future use
67
79
  @default_indent = 2
80
+ @default_line_ending = LINE_ENDING_LF
68
81
  @entity_encoding = :basic
69
82
  @restore_entities = false
70
83
  @preload_entity_sets = []
@@ -100,6 +113,7 @@ module Moxml
100
113
  end
101
114
 
102
115
  def entity_load_mode=(mode)
116
+ mode = mode.to_sym
103
117
  unless ENTITY_LOAD_MODES.include?(mode)
104
118
  raise ArgumentError,
105
119
  "Invalid entity_load_mode: #{mode}. Must be one of: #{ENTITY_LOAD_MODES.join(', ')}"
data/lib/moxml/context.rb CHANGED
@@ -17,13 +17,12 @@ module Moxml
17
17
  end
18
18
 
19
19
  def parse(xml, options = {})
20
- # Detect if input has XML declaration
21
- xml_string = if xml.respond_to?(:read)
20
+ xml_string = if xml.is_a?(String)
21
+ xml
22
+ else
22
23
  xml.read.tap do
23
- xml.rewind if xml.respond_to?(:rewind)
24
+ xml.rewind if xml.is_a?(IO) || xml.is_a?(StringIO)
24
25
  end
25
- else
26
- xml.to_s
27
26
  end
28
27
  has_declaration = xml_string.strip.start_with?("<?xml")
29
28
 
@@ -59,9 +58,6 @@ module Moxml
59
58
  # end
60
59
  #
61
60
  def sax_parse(xml, handler = nil, &block)
62
- # Load SAX module if not already loaded
63
- require_relative "sax" unless defined?(Moxml::SAX)
64
-
65
61
  # Create block handler if block given
66
62
  handler ||= SAX::BlockHandler.new(&block) if block
67
63
 
@@ -75,6 +71,10 @@ module Moxml
75
71
  config.adapter.sax_parse(xml, handler)
76
72
  end
77
73
 
74
+ def build(&block)
75
+ Builder.new(self).build(&block)
76
+ end
77
+
78
78
  private
79
79
 
80
80
  def build_entity_registry
@@ -34,13 +34,8 @@ module Moxml
34
34
  end
35
35
 
36
36
  def remove
37
- # Mark document as having no declaration when declaration is removed
38
- # Store in adapter's attachment map so all wrappers see it
39
- native_doc = adapter.document(@native)
40
- if native_doc && adapter.respond_to?(:attachments)
41
- adapter.attachments.set(native_doc, :has_declaration, false)
42
- end
43
-
37
+ native_doc = @parent_node&.native
38
+ adapter.remove_declaration(native_doc) if native_doc
44
39
  super
45
40
  end
46
41
 
@@ -1,16 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node"
4
- require_relative "element"
5
- require_relative "text"
6
- require_relative "cdata"
7
- require_relative "comment"
8
- require_relative "processing_instruction"
9
- require_relative "declaration"
10
- require_relative "namespace"
11
- require_relative "doctype"
12
- require_relative "entity_reference"
13
-
14
3
  module Moxml
15
4
  class Document < Node
16
5
  attr_accessor :has_xml_declaration
@@ -81,14 +70,8 @@ module Moxml
81
70
  if node.is_a?(Declaration)
82
71
  # Mark that document now has a declaration
83
72
  @has_xml_declaration = true
84
-
85
- if children.empty?
86
- adapter.add_child(@native, node.native)
87
- else
88
- adapter.add_previous_sibling(adapter.children(@native).first,
89
- node.native)
90
- end
91
- elsif root && !node.is_a?(ProcessingInstruction) && !node.is_a?(Comment)
73
+ adapter.add_child(@native, node.native)
74
+ elsif root && !node.is_a?(ProcessingInstruction) && !node.is_a?(Comment) && !node.is_a?(Doctype)
92
75
  raise Error, "Document already has a root element"
93
76
  else
94
77
  adapter.add_child(@native, node.native)
@@ -13,15 +13,13 @@ module Moxml
13
13
  @current_doc = context.create_document(native_doc)
14
14
 
15
15
  # Transfer has_declaration flag if present in attachments
16
- if adapter.respond_to?(:attachments) &&
17
- adapter.attachments.key?(native_doc, :has_declaration)
16
+ if adapter.attachments.key?(native_doc, :has_declaration)
18
17
  has_declaration = adapter.attachments.get(native_doc, :has_declaration)
19
18
  @current_doc.has_xml_declaration = has_declaration
20
19
  end
21
20
 
22
21
  # Transfer DOCTYPE from parsed document if it exists in attachments
23
- if adapter.respond_to?(:attachments) &&
24
- adapter.attachments.key?(native_doc, :doctype)
22
+ if adapter.attachments.key?(native_doc, :doctype)
25
23
  doctype = adapter.attachments.get(native_doc, :doctype)
26
24
  if doctype
27
25
  adapter.attachments.set(@current_doc.native, :doctype, doctype)
@@ -35,10 +33,16 @@ module Moxml
35
33
  private
36
34
 
37
35
  def visit_node(node)
38
- method_name = "visit_#{node_type(node)}"
39
- return unless respond_to?(method_name, true)
40
-
41
- send(method_name, node)
36
+ case node_type(node)
37
+ when :document then visit_document(node)
38
+ when :element then visit_element(node)
39
+ when :text then visit_text(node)
40
+ when :cdata then visit_cdata(node)
41
+ when :comment then visit_comment(node)
42
+ when :processing_instruction then visit_processing_instruction(node)
43
+ when :doctype then visit_doctype(node)
44
+ when :entity_reference then visit_entity_reference(node)
45
+ end
42
46
  end
43
47
 
44
48
  def visit_document(doc)
data/lib/moxml/element.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "attribute"
4
- require_relative "namespace"
5
-
6
3
  module Moxml
7
4
  class Element < Node
8
5
  def name
@@ -137,6 +134,8 @@ module Moxml
137
134
  adapter.restore_entities(val)
138
135
  end
139
136
 
137
+ alias content text
138
+
140
139
  def text=(content)
141
140
  adapter.set_text_content(@native, normalize_xml_value(content))
142
141
  invalidate_children_cache!
@@ -158,7 +157,8 @@ module Moxml
158
157
  end
159
158
 
160
159
  def inner_xml=(xml)
161
- doc = context.parse("<root>#{xml}</root>")
160
+ wrapper = "_moxml_inner_#{Process.pid}_#{object_id}"
161
+ doc = context.parse("<#{wrapper}>#{xml}</#{wrapper}>")
162
162
  adapter.replace_children(@native, doc.root.children.map(&:native))
163
163
  invalidate_children_cache!
164
164
  end
@@ -226,7 +226,8 @@ module Moxml
226
226
  # kept for backward compatibility.
227
227
  # @return [self]
228
228
  def load_html5
229
- warn "EntityRegistry#load_html5 is a no-op (all entities load during initialize)", uplevel: 1
229
+ warn "EntityRegistry#load_html5 is a no-op (all entities load during initialize)",
230
+ uplevel: 1
230
231
  self
231
232
  end
232
233
 
@@ -235,7 +236,8 @@ module Moxml
235
236
  # kept for backward compatibility.
236
237
  # @return [self]
237
238
  def load_mathml
238
- warn "EntityRegistry#load_mathml is a no-op (all entities load during initialize)", uplevel: 1
239
+ warn "EntityRegistry#load_mathml is a no-op (all entities load during initialize)",
240
+ uplevel: 1
239
241
  self
240
242
  end
241
243
 
@@ -245,7 +247,8 @@ module Moxml
245
247
  # @param _set_name [Symbol] (ignored, all loaded together)
246
248
  # @return [self]
247
249
  def load_iso(_set_name = :iso8879)
248
- warn "EntityRegistry#load_iso is a no-op (all entities load during initialize)", uplevel: 1
250
+ warn "EntityRegistry#load_iso is a no-op (all entities load during initialize)",
251
+ uplevel: 1
249
252
  self
250
253
  end
251
254
 
@@ -254,7 +257,8 @@ module Moxml
254
257
  # kept for backward compatibility.
255
258
  # @return [self]
256
259
  def load_all
257
- warn "EntityRegistry#load_all is a no-op (all entities load during initialize)", uplevel: 1
260
+ warn "EntityRegistry#load_all is a no-op (all entities load during initialize)",
261
+ uplevel: 1
258
262
  self
259
263
  end
260
264
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # Auto-generated entity data for Opal runtime.
4
- # Generated from data/w3c_entities.json
5
- # Do not edit manually. Regenerate with: rake opal:generate_entity_data
5
+ # Source: data/w3c_entities.json (2125 entities)
6
+ # Regenerate with: rake opal:generate_entity_data
6
7
 
7
8
  module Moxml
8
9
  class EntityRegistry
data/lib/moxml/node.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "xml_utils"
4
- require_relative "node_set"
5
-
6
3
  module Moxml
7
4
  class Node
8
5
  include XmlUtils
6
+ include Enumerable
9
7
 
10
8
  TYPES = %i[
11
9
  element text cdata comment processing_instruction document
@@ -98,6 +96,7 @@ module Moxml
98
96
  serialize_options[:no_declaration] = !should_include_declaration?(options)
99
97
 
100
98
  result = adapter.serialize(@native, serialize_options)
99
+ result = apply_line_ending(result, serialize_options[:line_ending])
101
100
 
102
101
  # Restore entity markers to named entity references
103
102
  adapter.restore_entities(result)
@@ -142,13 +141,14 @@ module Moxml
142
141
  ""
143
142
  end
144
143
 
145
- # Attribute accessor - only works on Element nodes
146
- # Returns nil for non-element nodes
147
- def [](name)
148
- return nil unless respond_to?(:attribute)
149
-
150
- attr = attribute(name)
151
- attr&.value if attr.respond_to?(:value)
144
+ # Returns the content/value of this node as a string.
145
+ # Each subclass overrides this with type-specific semantics:
146
+ # - Text, Comment, Cdata: raw text content
147
+ # - ProcessingInstruction: instruction content
148
+ # - Attribute: attribute value
149
+ # - Element: delegates to text (descendant text concatenation)
150
+ def content
151
+ ""
152
152
  end
153
153
 
154
154
  # Returns the namespace of this node
@@ -175,15 +175,39 @@ module Moxml
175
175
  def each_node(&block)
176
176
  children.each do |child|
177
177
  yield child
178
- child.each_node(&block) if child.respond_to?(:each_node)
178
+ child.each_node(&block)
179
179
  end
180
180
  end
181
181
 
182
- # Clone node (deep copy)
183
- def clone
182
+ # Yield direct children, enabling Enumerable on the node.
183
+ def each(&block)
184
+ return to_enum(:each) unless block
185
+
186
+ children.each(&block)
187
+ end
188
+
189
+ def outer_xml
190
+ to_xml
191
+ end
192
+
193
+ def before(node)
194
+ add_previous_sibling(node)
195
+ end
196
+
197
+ def after(node)
198
+ add_next_sibling(node)
199
+ end
200
+
201
+ def blank?
202
+ text.strip.empty?
203
+ end
204
+
205
+ # Deep copy of the node (both dup and clone create deep copies for XML nodes)
206
+ def dup
184
207
  Moxml::Node.wrap(adapter.dup(@native), context)
185
208
  end
186
- alias dup clone
209
+
210
+ alias clone dup
187
211
 
188
212
  def ==(other)
189
213
  self.class == other.class && @native == other.native
@@ -279,6 +303,7 @@ module Moxml
279
303
  {
280
304
  encoding: context.config.default_encoding,
281
305
  indent: context.config.default_indent,
306
+ line_ending: context.config.default_line_ending,
282
307
  # The short format of empty tags in Oga and Nokogiri isn't configurable
283
308
  # Oga: <empty /> (with a space)
284
309
  # Nokogiri: <empty/> (without a space)
@@ -294,5 +319,11 @@ module Moxml
294
319
  # For Document nodes, delegate to adapter for native state check
295
320
  adapter.has_declaration?(@native, self)
296
321
  end
322
+
323
+ def apply_line_ending(xml, line_ending)
324
+ return xml if line_ending == Config::LINE_ENDING_LF || !xml.include?("\n")
325
+
326
+ xml.gsub(/\r?\n/, line_ending)
327
+ end
297
328
  end
298
329
  end
@@ -68,8 +68,7 @@ module Moxml
68
68
  end
69
69
 
70
70
  def <<(node)
71
- # If it's a wrapped Moxml node, unwrap to native before storing
72
- native_node = node.respond_to?(:native) ? node.native : node
71
+ native_node = node.is_a?(Node) ? node.native : node
73
72
  @nodes << native_node
74
73
  @wrapped << nil
75
74
  self
@@ -113,8 +112,7 @@ module Moxml
113
112
  # Delete a node from the set
114
113
  # Accepts both wrapped Moxml nodes and native nodes
115
114
  def delete(node)
116
- # If it's a wrapped Moxml node, unwrap to native
117
- native_node = node.respond_to?(:native) ? node.native : node
115
+ native_node = node.is_a?(Node) ? node.native : node
118
116
  idx = @nodes.index(native_node)
119
117
  if idx
120
118
  @nodes.delete_at(idx)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "handler"
4
-
5
3
  module Moxml
6
4
  module SAX
7
5
  # Block-based SAX handler with DSL