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
@@ -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
  # Element-focused SAX handler with stack tracking
@@ -38,11 +38,12 @@ module Moxml
38
38
  when nil
39
39
  # nothing
40
40
  else
41
- if attributes.respond_to?(:each)
41
+ if attributes.is_a?(Enumerable)
42
42
  attributes.each do |item|
43
- if item.is_a?(Array) && item.size >= 2
44
- yield item[0], item[1]
45
- elsif item.respond_to?(:name) && item.respond_to?(:value)
43
+ case item
44
+ when Array
45
+ yield item[0], item[1] if item.size >= 2
46
+ else
46
47
  yield item.name, item.value
47
48
  end
48
49
  end
data/lib/moxml/sax.rb CHANGED
@@ -1,31 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "sax/handler"
4
- require_relative "sax/element_handler"
5
- require_relative "sax/block_handler"
6
-
7
3
  module Moxml
8
- # SAX (Simple API for XML) parsing interface
9
- #
10
- # Provides event-driven XML parsing across all Moxml adapters.
11
- # SAX parsing is memory-efficient and suitable for processing large XML files.
12
- #
13
- # @example Class-based handler
14
- # class MyHandler < Moxml::SAX::Handler
15
- # def on_start_element(name, attributes = {}, namespaces = {})
16
- # puts "Started element: #{name}"
17
- # end
18
- # end
19
- #
20
- # context = Moxml.new
21
- # context.sax_parse(xml_string, MyHandler.new)
22
- #
23
- # @example Block-based handler
24
- # context.sax_parse(xml_string) do
25
- # start_element { |name, attrs| puts "Element: #{name}" }
26
- # characters { |text| puts "Text: #{text}" }
27
- # end
28
- #
29
4
  module SAX
5
+ autoload :Handler, "moxml/sax/handler"
6
+ autoload :ElementHandler, "moxml/sax/element_handler"
7
+ autoload :BlockHandler, "moxml/sax/block_handler"
8
+ autoload :NamespaceSplitter, "moxml/sax/namespace_splitter"
30
9
  end
31
10
  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.1.22"
4
+ VERSION = "0.1.24"
5
5
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "xml_utils/encoder"
4
-
5
- # Ruby 3.3+ requires the URI module to be explicitly required
6
3
  require "uri" unless defined?(URI)
7
4
 
8
5
  module Moxml
9
6
  module XmlUtils
7
+ autoload :Encoder, "moxml/xml_utils/encoder"
10
8
  def encode_entities(text, mode = nil)
11
9
  Encoder.new(text, mode).call
12
10
  end
@@ -82,6 +80,7 @@ module Moxml
82
80
  else
83
81
  # Accept relative references and bare paths
84
82
  return unless uri.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F]/)
83
+
85
84
  raise ValidationError, "Invalid URI: #{uri}"
86
85
  end
87
86
  rescue URI::InvalidURIError
@@ -62,16 +62,6 @@ module Moxml
62
62
  matched = matched_literal
63
63
  context_var = context_literal
64
64
 
65
- # Enable debug output
66
- debug = ENV["DEBUG_XPATH"] == "1"
67
- if debug
68
- puts "\n#{'=' * 60}"
69
- puts "COMPILING XPath"
70
- puts "=" * 60
71
- puts "AST: #{ast.inspect}"
72
- puts
73
- end
74
-
75
65
  ruby_ast = if return_nodeset?(ast)
76
66
  process(ast, document) { |node| matched.push(node) }
77
67
  else
@@ -103,14 +93,6 @@ module Moxml
103
93
  generator = Ruby::Generator.new
104
94
  source = generator.process(proc_ast)
105
95
 
106
- if debug
107
- puts "GENERATED RUBY CODE:"
108
- puts "-" * 60
109
- puts source
110
- puts "=" * 60
111
- puts
112
- end
113
-
114
96
  CONTEXT.evaluate(source)
115
97
  ensure
116
98
  @literal_id = 0
@@ -1506,7 +1488,7 @@ module Moxml
1506
1488
  xml_lang.assign(string("xml:lang"))
1507
1489
  end
1508
1490
  .followed_by do
1509
- node.respond_to?(symbol(:attribute)).while_true do
1491
+ node.is_a?(const_ref("Moxml", "Element")).while_true do
1510
1492
  found.assign(node.get(xml_lang))
1511
1493
  .followed_by do
1512
1494
  found.if_true do
@@ -1622,36 +1604,6 @@ module Moxml
1622
1604
  arg_var.assign(arg_ast).followed_by { yield arg_var }
1623
1605
  end
1624
1606
 
1625
- # Helper: Try to match first node v1
1626
- def try_match_first_node_v1(ast, input, optimize_first = true)
1627
- if return_nodeset?(ast) && optimize_first
1628
- matched_set = unique_literal(:matched_set)
1629
- first_node = unique_literal(:first_node)
1630
- context_var = context_literal
1631
-
1632
- # Create NodeSet for results
1633
- nodeset_class = const_ref("Moxml", "NodeSet")
1634
- empty_array = Ruby::Node.new(:array, [])
1635
- nodeset_new = Ruby::Node.new(:send,
1636
- [nodeset_class, "new", empty_array,
1637
- context_var])
1638
-
1639
- matched_set.assign(nodeset_new)
1640
- .followed_by do
1641
- # Process with block to accumulate results
1642
- process(ast, input) { |node| matched_set.push(node) }
1643
- end
1644
- .followed_by do
1645
- first_node.assign(matched_set[literal(0)])
1646
- end
1647
- .followed_by do
1648
- first_node.if_true { first_node }.else { string("") }
1649
- end
1650
- else
1651
- process(ast, input)
1652
- end
1653
- end
1654
-
1655
1607
  # Helper: Create mass assignment node
1656
1608
  def mass_assign(vars, value)
1657
1609
  Ruby::Node.new(:massign, [vars, value])
@@ -12,11 +12,11 @@ module Moxml
12
12
  # @param [Object] right
13
13
  # @return [Array<Object, Object>]
14
14
  def self.to_compatible_types(left, right)
15
- if left.is_a?(Moxml::NodeSet) || left.respond_to?(:text)
15
+ if left.is_a?(Moxml::NodeSet) || left.is_a?(Moxml::Node)
16
16
  left = to_string(left)
17
17
  end
18
18
 
19
- if right.is_a?(Moxml::NodeSet) || right.respond_to?(:text)
19
+ if right.is_a?(Moxml::NodeSet) || right.is_a?(Moxml::Node)
20
20
  right = to_string(right)
21
21
  end
22
22
 
@@ -51,7 +51,7 @@ module Moxml
51
51
  value = first_node_text(value)
52
52
  end
53
53
 
54
- if value.respond_to?(:text)
54
+ if value.is_a?(Moxml::Node)
55
55
  value = value.text
56
56
  end
57
57
 
@@ -67,7 +67,7 @@ module Moxml
67
67
  value = first_node_text(value)
68
68
  end
69
69
 
70
- if value.respond_to?(:text)
70
+ if value.is_a?(Moxml::Node)
71
71
  value = value.text
72
72
  end
73
73
 
@@ -95,7 +95,7 @@ module Moxml
95
95
  bool = !value.nan? && !value.zero?
96
96
  elsif value.is_a?(Integer)
97
97
  bool = !value.zero?
98
- elsif value.respond_to?(:empty?)
98
+ elsif value.is_a?(String) || value.is_a?(Moxml::NodeSet) || value.is_a?(Array)
99
99
  bool = !value.empty?
100
100
  elsif value
101
101
  bool = true
@@ -117,7 +117,8 @@ module Moxml
117
117
  # @param [Moxml::NodeSet] set
118
118
  # @return [String]
119
119
  def self.first_node_text(set)
120
- set[0].respond_to?(:text) ? set[0].text : ""
120
+ first = set[0]
121
+ first.is_a?(Moxml::Node) ? first.text : ""
121
122
  end
122
123
  end
123
124
  end
@@ -13,13 +13,7 @@ module Moxml
13
13
  # @param [Moxml::XPath::Ruby::Node] ast
14
14
  # @return [String]
15
15
  def process(ast)
16
- handler = :"on_#{ast.type}"
17
- unless respond_to?(handler, true)
18
- raise NotImplementedError,
19
- "Generator missing handler for node type :#{ast.type}. Node: #{ast.inspect}"
20
- end
21
-
22
- send(handler, ast)
16
+ send(:"on_#{ast.type}", ast)
23
17
  end
24
18
 
25
19
  # @param [Moxml::XPath::Ruby::Node] ast
@@ -81,6 +75,15 @@ module Moxml
81
75
  "#{left_str} == #{right_str}"
82
76
  end
83
77
 
78
+ def on_neq(ast)
79
+ left, right = *ast
80
+
81
+ left_str = process(left)
82
+ right_str = process(right)
83
+
84
+ "#{left_str} != #{right_str}"
85
+ end
86
+
84
87
  # Processes a boolean "and" node.
85
88
  #
86
89
  # @param [Moxml::XPath::Ruby::Node] ast
@@ -167,14 +170,8 @@ module Moxml
167
170
  brackets = name == "[]"
168
171
 
169
172
  unless args.empty?
170
- arg_strs = []
171
- args.each do |arg|
172
- result = process(arg)
173
- # Keep processing if we got a Node back (happens with nested send nodes)
174
- while result.respond_to?(:type)
175
- result = process(result)
176
- end
177
- arg_strs << result
173
+ arg_strs = args.map do |arg|
174
+ process(arg)
178
175
  end
179
176
  arg_str = arg_strs.join(", ")
180
177
  call = brackets ? "[#{arg_str}]" : "#{call}(#{arg_str})"
@@ -182,10 +179,6 @@ module Moxml
182
179
 
183
180
  if receiver
184
181
  rec_str = process(receiver)
185
- # Keep processing if we got a Node back
186
- while rec_str.respond_to?(:type)
187
- rec_str = process(rec_str)
188
- end
189
182
  call = brackets ? "#{rec_str}#{call}" : "#{rec_str}.#{call}"
190
183
  end
191
184
 
@@ -95,15 +95,7 @@ module Moxml
95
95
  # @param [Class] klass
96
96
  # @return [Moxml::XPath::Ruby::Node]
97
97
  def is_a?(klass)
98
- # If klass is already a Node (e.g., a const node), use it directly
99
- # Otherwise wrap it in a lit node
100
- klass_node = if klass.respond_to?(:type)
101
- klass
102
- else
103
- Node.new(:lit, [klass.to_s])
104
- end
105
-
106
- Node.new(:send, [self, "is_a?", klass_node])
98
+ Node.new(:send, [self, "is_a?", klass])
107
99
  end
108
100
 
109
101
  # Wraps the current node in a block.
data/lib/moxml/xpath.rb CHANGED
@@ -1,16 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- # XPath 1.0 implementation for Moxml
5
- #
6
- # This module provides a complete XPath 1.0 engine for querying XML
7
- # documents, particularly for the Ox adapter which has limited native
8
- # XPath support.
9
- #
10
- # @example Basic usage
11
- # engine = Moxml::XPath::Engine.new(document)
12
- # results = engine.evaluate("//book[@id='123']")
13
- #
14
4
  module XPath
15
5
  autoload :Engine, "moxml/xpath/engine"
16
6
  autoload :Context, "moxml/xpath/context"
@@ -20,15 +10,17 @@ module Moxml
20
10
  autoload :Parser, "moxml/xpath/parser"
21
11
  autoload :Compiler, "moxml/xpath/compiler"
22
12
 
23
- # Require errors directly so classes are immediately available
24
- require_relative "xpath/errors"
13
+ autoload :Error, "moxml/xpath/errors"
14
+ autoload :SyntaxError, "moxml/xpath/errors"
15
+ autoload :EvaluationError, "moxml/xpath/errors"
16
+ autoload :FunctionError, "moxml/xpath/errors"
17
+ autoload :NodeTypeError, "moxml/xpath/errors"
18
+ autoload :InvalidContextError, "moxml/xpath/errors"
25
19
 
26
- # AST nodes for expression representation
27
20
  module AST
28
21
  autoload :Node, "moxml/xpath/ast/node"
29
22
  end
30
23
 
31
- # Ruby AST generation for compiling XPath
32
24
  module Ruby
33
25
  autoload :Node, "moxml/xpath/ruby/node"
34
26
  autoload :Generator, "moxml/xpath/ruby/generator"
data/lib/moxml.rb CHANGED
@@ -8,13 +8,30 @@ module Moxml
8
8
  context
9
9
  end
10
10
 
11
+ def parse(xml, adapter: nil, **options)
12
+ Context.new(adapter).parse(xml, options)
13
+ end
14
+
11
15
  def configure
12
16
  yield Config.default if block_given?
13
17
  end
14
18
 
15
19
  def with_config(adapter_name = nil, strict_parsing = nil,
16
20
  default_encoding = nil)
17
- original_config = Config.default.dup
21
+ original = Config.default
22
+ saved_values = {
23
+ adapter: original.adapter_name,
24
+ strict_parsing: original.strict_parsing,
25
+ default_encoding: original.default_encoding,
26
+ default_indent: original.default_indent,
27
+ default_line_ending: original.default_line_ending,
28
+ entity_load_mode: original.entity_load_mode,
29
+ entity_encoding: original.entity_encoding,
30
+ restore_entities: original.restore_entities,
31
+ namespace_validation_mode: original.namespace_validation_mode,
32
+ entity_restoration_mode: original.entity_restoration_mode,
33
+ preload_entity_sets: original.preload_entity_sets.dup,
34
+ }
18
35
 
19
36
  configure do |config|
20
37
  config.adapter = adapter_name unless adapter_name.nil?
@@ -23,27 +40,64 @@ module Moxml
23
40
  end
24
41
 
25
42
  yield if block_given?
26
-
27
- # restore the original config
43
+ ensure
28
44
  configure do |config|
29
- config.adapter = original_config.adapter_name
30
- config.strict_parsing = original_config.strict_parsing
31
- config.default_encoding = original_config.default_encoding
45
+ config.adapter = saved_values[:adapter]
46
+ config.strict_parsing = saved_values[:strict_parsing]
47
+ config.default_encoding = saved_values[:default_encoding]
48
+ config.default_indent = saved_values[:default_indent]
49
+ config.default_line_ending = saved_values[:default_line_ending]
50
+ config.entity_load_mode = saved_values[:entity_load_mode]
51
+ config.entity_encoding = saved_values[:entity_encoding]
52
+ config.restore_entities = saved_values[:restore_entities]
53
+ config.namespace_validation_mode = saved_values[:namespace_validation_mode]
54
+ config.entity_restoration_mode = saved_values[:entity_restoration_mode]
55
+ config.preload_entity_sets = saved_values[:preload_entity_sets]
32
56
  end
33
- original_config = nil
57
+ end
58
+
59
+ def preprocess_entities(xml)
60
+ Adapter::Base.preprocess_entities(xml)
61
+ end
62
+
63
+ def restore_entities(text)
64
+ Adapter::Base.restore_entities(text)
34
65
  end
35
66
  end
36
- end
37
67
 
38
- require_relative "moxml/version"
39
- require_relative "moxml/document"
40
- require_relative "moxml/document_builder"
41
- require_relative "moxml/error"
42
- require_relative "moxml/builder"
43
- require_relative "moxml/config"
44
- require_relative "moxml/context"
45
- require_relative "moxml/entity_registry"
46
- require_relative "moxml/native_attachment"
47
- require_relative "moxml/adapter"
48
- require_relative "moxml/xpath"
49
- require_relative "moxml/sax"
68
+ autoload :VERSION, "moxml/version"
69
+ autoload :Config, "moxml/config"
70
+ autoload :Context, "moxml/context"
71
+ autoload :Node, "moxml/node"
72
+ autoload :NodeSet, "moxml/node_set"
73
+ autoload :Document, "moxml/document"
74
+ autoload :Element, "moxml/element"
75
+ autoload :Text, "moxml/text"
76
+ autoload :Cdata, "moxml/cdata"
77
+ autoload :Comment, "moxml/comment"
78
+ autoload :Attribute, "moxml/attribute"
79
+ autoload :ProcessingInstruction, "moxml/processing_instruction"
80
+ autoload :Declaration, "moxml/declaration"
81
+ autoload :Namespace, "moxml/namespace"
82
+ autoload :Doctype, "moxml/doctype"
83
+ autoload :EntityReference, "moxml/entity_reference"
84
+ autoload :DocumentBuilder, "moxml/document_builder"
85
+ autoload :Builder, "moxml/builder"
86
+ autoload :EntityRegistry, "moxml/entity_registry"
87
+ autoload :NativeAttachment, "moxml/native_attachment"
88
+ autoload :XmlUtils, "moxml/xml_utils"
89
+ autoload :Adapter, "moxml/adapter"
90
+ autoload :XPath, "moxml/xpath"
91
+ autoload :SAX, "moxml/sax"
92
+
93
+ # Error hierarchy — each subclass autoloads from the same file
94
+ autoload :Error, "moxml/error"
95
+ autoload :NotImplementedError, "moxml/error"
96
+ autoload :ValidationError, "moxml/error"
97
+ autoload :ParseError, "moxml/error"
98
+ autoload :DocumentStructureError, "moxml/error"
99
+ autoload :NamespaceError, "moxml/error"
100
+ autoload :EntityDataError, "moxml/error"
101
+ autoload :XPathError, "moxml/error"
102
+ autoload :AdapterError, "moxml/error"
103
+ end
@@ -29,6 +29,7 @@ RSpec.describe "Cross-adapter integration" do
29
29
  "Memory Usage Examples",
30
30
  "Thread Safety Examples",
31
31
  "Entity Reference Whitespace Preservation",
32
+ "Moxml Line Ending",
32
33
  "Performance Examples",
33
34
  ]
34
35
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "Moxml Line Ending" do
4
+ describe "Line ending configuration" do
5
+ let(:context) { Moxml.new }
6
+ let(:xml) { "<root><child>text</child></root>" }
7
+
8
+ it "produces no CRLF with LF default" do
9
+ doc = context.parse(xml)
10
+ expect(doc.to_xml).not_to include("\r\n")
11
+ end
12
+
13
+ it "produces no bare LF with CRLF configured" do
14
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
15
+ doc = context.parse(xml)
16
+ expect(doc.to_xml).not_to match(/(?<!\r)\n/)
17
+ end
18
+
19
+ it "allows per-call CRLF override producing no bare LF" do
20
+ doc = context.parse(xml)
21
+ output = doc.to_xml(line_ending: Moxml::Config::LINE_ENDING_CRLF)
22
+ expect(output).not_to match(/(?<!\r)\n/)
23
+ end
24
+
25
+ it "per-call LF override wins over config CRLF" do
26
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
27
+ doc = context.parse(xml)
28
+ expect(doc.to_xml(line_ending: Moxml::Config::LINE_ENDING_LF))
29
+ .not_to include("\r\n")
30
+ end
31
+
32
+ it "produces identical bytes on re-serialization with CRLF" do
33
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
34
+ doc = context.parse(xml)
35
+ first = doc.to_xml
36
+
37
+ ctx2 = Moxml.new
38
+ ctx2.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
39
+ result = ctx2.parse(first)
40
+ expect(result.to_xml).to eq(first)
41
+ end
42
+
43
+ it "preserves element structure through CRLF round-trip" do
44
+ doc = context.parse("<root><a>text</a><b>more</b></root>")
45
+ context.config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
46
+ crlf_output = doc.to_xml
47
+
48
+ ctx2 = Moxml.new
49
+ result = ctx2.parse(crlf_output)
50
+ elements = result.root.children.select(&:element?)
51
+ expect(elements.map(&:name)).to eq(%w[a b])
52
+ expect(elements[0].children.first.content).to eq("text")
53
+ expect(elements[1].children.first.content).to eq("more")
54
+ end
55
+ end
56
+ end
@@ -105,7 +105,8 @@ RSpec.describe Moxml::Adapter::Libxml do
105
105
 
106
106
  it "returns [nil, nil] when the document has no entity-ref attachments" do
107
107
  root = libxml_native(context.parse("<root><a/></root>").root)
108
- expect(adapter.send(:lookup_entity_ref_serialization, root)).to eq([nil, nil])
108
+ expect(adapter.send(:lookup_entity_ref_serialization,
109
+ root)).to eq([nil, nil])
109
110
  end
110
111
 
111
112
  it "returns [nil, nil] for an element with no entity refs even when the doc has erefs elsewhere" do
@@ -129,7 +130,8 @@ RSpec.describe Moxml::Adapter::Libxml do
129
130
  )
130
131
  a.add_child(eref)
131
132
 
132
- refs, seq = adapter.send(:lookup_entity_ref_serialization, libxml_native(a))
133
+ refs, seq = adapter.send(:lookup_entity_ref_serialization,
134
+ libxml_native(a))
133
135
  expect(refs).to be_an(Array).and(satisfy { |r| !r.empty? })
134
136
  expect(seq).to be_an(Array).and(include(:eref))
135
137
  end
@@ -4,7 +4,8 @@ require "spec_helper"
4
4
 
5
5
  RSpec.describe Moxml::Adapter, ".platform_adapters" do
6
6
  it "includes all known adapters under MRI" do
7
- expect(described_class.platform_adapters).to include(:nokogiri, :oga, :rexml, :ox)
7
+ expect(described_class.platform_adapters).to include(:nokogiri, :oga,
8
+ :rexml, :ox)
8
9
  end
9
10
 
10
11
  it "uses the AVAILABLE_ADAPTERS constant under MRI" do
@@ -27,4 +27,20 @@ RSpec.describe Moxml::Attribute do
27
27
  expect(attr.to_s).to match(/\w+="\w+"/)
28
28
  end
29
29
  end
30
+
31
+ describe "#element" do
32
+ it "returns wrapped Element" do
33
+ attr = element.attributes.find { |a| a.name == "id" }
34
+ parent = attr.element
35
+ expect(parent).to be_a(Moxml::Element)
36
+ expect(parent.name).to eq("root")
37
+ end
38
+ end
39
+
40
+ describe "#content" do
41
+ it "returns attribute value" do
42
+ attr = element.attributes.find { |a| a.name == "id" }
43
+ expect(attr.content).to eq("123")
44
+ end
45
+ end
30
46
  end
@@ -10,6 +10,7 @@ RSpec.describe Moxml::Config do
10
10
  expect(config.strict_parsing).to be true
11
11
  expect(config.default_encoding).to eq("UTF-8")
12
12
  expect(config.default_indent).to eq(2)
13
+ expect(config.default_line_ending).to eq("\n")
13
14
  expect(config.entity_encoding).to eq(:basic)
14
15
  end
15
16
 
@@ -89,6 +90,38 @@ RSpec.describe Moxml::Config do
89
90
  end
90
91
  end
91
92
 
93
+ describe "#default_line_ending=" do
94
+ it "accepts LINE_ENDING_LF" do
95
+ config.default_line_ending = Moxml::Config::LINE_ENDING_LF
96
+ expect(config.default_line_ending).to eq("\n")
97
+ end
98
+
99
+ it "accepts LINE_ENDING_CRLF" do
100
+ config.default_line_ending = Moxml::Config::LINE_ENDING_CRLF
101
+ expect(config.default_line_ending).to eq("\r\n")
102
+ end
103
+
104
+ it "rejects nil" do
105
+ expect { config.default_line_ending = nil }
106
+ .to raise_error(ArgumentError, /Invalid line_ending/)
107
+ end
108
+
109
+ it "rejects arbitrary strings" do
110
+ expect { config.default_line_ending = "BAD" }
111
+ .to raise_error(ArgumentError, /Invalid line_ending/)
112
+ end
113
+
114
+ it "rejects bare CR" do
115
+ expect { config.default_line_ending = "\r" }
116
+ .to raise_error(ArgumentError, /Invalid line_ending/)
117
+ end
118
+
119
+ it "rejects empty string" do
120
+ expect { config.default_line_ending = "" }
121
+ .to raise_error(ArgumentError, /Invalid line_ending/)
122
+ end
123
+ end
124
+
92
125
  describe "#adapter=" do
93
126
  it "sets valid adapter" do
94
127
  config.adapter = :ox
@@ -25,4 +25,18 @@ RSpec.describe Moxml::Context do
25
25
  expect(context.config.adapter.ancestors).to include(Moxml::Adapter::Base)
26
26
  end
27
27
  end
28
+
29
+ describe "#build" do
30
+ it "creates a document via builder DSL" do
31
+ doc = context.build do
32
+ element("root") do
33
+ element("child") do
34
+ text("hello")
35
+ end
36
+ end
37
+ end
38
+ expect(doc).to be_a(Moxml::Document)
39
+ expect(doc.root.name).to eq("root")
40
+ end
41
+ end
28
42
  end
@@ -52,4 +52,17 @@ RSpec.describe Moxml do
52
52
  expect(Moxml::Config::VALID_ADAPTERS).to include(Moxml::Config.runtime_default_adapter)
53
53
  end
54
54
  end
55
+
56
+ describe ".parse" do
57
+ it "parses XML with shorthand" do
58
+ doc = described_class.parse("<root>hello</root>")
59
+ expect(doc).to be_a(Moxml::Document)
60
+ expect(doc.root.text).to eq("hello")
61
+ end
62
+
63
+ it "accepts adapter option" do
64
+ doc = described_class.parse("<root/>", adapter: :rexml)
65
+ expect(doc).to be_a(Moxml::Document)
66
+ end
67
+ end
55
68
  end