moxml 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +23 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +2 -0
  6. data/.rubocop_todo.yml +65 -0
  7. data/.ruby-version +1 -0
  8. data/Gemfile +10 -3
  9. data/README.adoc +400 -594
  10. data/lib/moxml/adapter/base.rb +102 -0
  11. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +18 -0
  12. data/lib/moxml/adapter/customized_oga/xml_generator.rb +104 -0
  13. data/lib/moxml/adapter/nokogiri.rb +314 -0
  14. data/lib/moxml/adapter/oga.rb +309 -0
  15. data/lib/moxml/adapter/ox.rb +325 -0
  16. data/lib/moxml/adapter.rb +26 -170
  17. data/lib/moxml/attribute.rb +47 -14
  18. data/lib/moxml/builder.rb +64 -0
  19. data/lib/moxml/cdata.rb +4 -26
  20. data/lib/moxml/comment.rb +6 -22
  21. data/lib/moxml/config.rb +39 -15
  22. data/lib/moxml/context.rb +29 -0
  23. data/lib/moxml/declaration.rb +16 -26
  24. data/lib/moxml/doctype.rb +9 -0
  25. data/lib/moxml/document.rb +51 -63
  26. data/lib/moxml/document_builder.rb +87 -0
  27. data/lib/moxml/element.rb +61 -99
  28. data/lib/moxml/error.rb +20 -0
  29. data/lib/moxml/namespace.rb +12 -37
  30. data/lib/moxml/node.rb +78 -58
  31. data/lib/moxml/node_set.rb +19 -222
  32. data/lib/moxml/processing_instruction.rb +6 -25
  33. data/lib/moxml/text.rb +4 -26
  34. data/lib/moxml/version.rb +1 -1
  35. data/lib/moxml/xml_utils/encoder.rb +55 -0
  36. data/lib/moxml/xml_utils.rb +80 -0
  37. data/lib/moxml.rb +33 -33
  38. data/moxml.gemspec +1 -1
  39. data/spec/moxml/adapter/nokogiri_spec.rb +14 -0
  40. data/spec/moxml/adapter/oga_spec.rb +14 -0
  41. data/spec/moxml/adapter/ox_spec.rb +49 -0
  42. data/spec/moxml/all_with_adapters_spec.rb +46 -0
  43. data/spec/moxml/config_spec.rb +55 -0
  44. data/spec/moxml/error_spec.rb +71 -0
  45. data/spec/moxml/examples/adapter_spec.rb +27 -0
  46. data/spec/moxml_spec.rb +50 -0
  47. data/spec/spec_helper.rb +32 -0
  48. data/spec/support/shared_examples/attribute.rb +165 -0
  49. data/spec/support/shared_examples/builder.rb +25 -0
  50. data/spec/support/shared_examples/cdata.rb +70 -0
  51. data/spec/support/shared_examples/comment.rb +65 -0
  52. data/spec/support/shared_examples/context.rb +35 -0
  53. data/spec/support/shared_examples/declaration.rb +93 -0
  54. data/spec/support/shared_examples/doctype.rb +25 -0
  55. data/spec/support/shared_examples/document.rb +110 -0
  56. data/spec/support/shared_examples/document_builder.rb +43 -0
  57. data/spec/support/shared_examples/edge_cases.rb +185 -0
  58. data/spec/support/shared_examples/element.rb +110 -0
  59. data/spec/support/shared_examples/examples/attribute.rb +42 -0
  60. data/spec/support/shared_examples/examples/basic_usage.rb +67 -0
  61. data/spec/support/shared_examples/examples/memory.rb +54 -0
  62. data/spec/support/shared_examples/examples/namespace.rb +65 -0
  63. data/spec/support/shared_examples/examples/readme_examples.rb +100 -0
  64. data/spec/support/shared_examples/examples/thread_safety.rb +43 -0
  65. data/spec/support/shared_examples/examples/xpath.rb +39 -0
  66. data/spec/support/shared_examples/integration.rb +135 -0
  67. data/spec/support/shared_examples/namespace.rb +96 -0
  68. data/spec/support/shared_examples/node.rb +110 -0
  69. data/spec/support/shared_examples/node_set.rb +90 -0
  70. data/spec/support/shared_examples/processing_instruction.rb +88 -0
  71. data/spec/support/shared_examples/text.rb +66 -0
  72. data/spec/support/shared_examples/xml_adapter.rb +191 -0
  73. data/spec/support/xml_matchers.rb +27 -0
  74. metadata +55 -6
  75. data/.github/workflows/main.yml +0 -27
  76. data/lib/moxml/error_handler.rb +0 -77
  77. data/lib/moxml/errors.rb +0 -169
data/lib/moxml/adapter.rb CHANGED
@@ -1,175 +1,31 @@
1
- # lib/moxml/adapter.rb
2
- module Moxml
3
- class Adapter
4
- # Document operations
5
- def parse(input, options = {})
6
- raise NotImplementedError
7
- end
8
-
9
- def serialize(node, options = {})
10
- raise NotImplementedError
11
- end
12
-
13
- # Node type detection
14
- def node_type(node)
15
- raise NotImplementedError
16
- end
17
-
18
- # Node operations
19
- def node_name(node)
20
- raise NotImplementedError
21
- end
22
-
23
- def parent(node)
24
- raise NotImplementedError
25
- end
26
-
27
- def children(node)
28
- raise NotImplementedError
29
- end
30
-
31
- def next_sibling(node)
32
- raise NotImplementedError
33
- end
34
-
35
- def previous_sibling(node)
36
- raise NotImplementedError
37
- end
38
-
39
- def document(node)
40
- raise NotImplementedError
41
- end
42
-
43
- def remove(node)
44
- raise NotImplementedError
45
- end
46
-
47
- def replace(node, new_node)
48
- raise NotImplementedError
49
- end
50
-
51
- # Element operations
52
- def create_element(document, name)
53
- raise NotImplementedError
54
- end
1
+ # frozen_string_literal: true
55
2
 
56
- def attributes(element)
57
- raise NotImplementedError
58
- end
59
-
60
- def get_attribute(element, name)
61
- raise NotImplementedError
62
- end
63
-
64
- def set_attribute(element, name, value)
65
- raise NotImplementedError
66
- end
3
+ require_relative "adapter/base"
67
4
 
68
- def remove_attribute(element, name)
69
- raise NotImplementedError
70
- end
71
-
72
- def add_child(element, child)
73
- raise NotImplementedError
74
- end
75
-
76
- # Namespace operations
77
- def namespaces(element)
78
- raise NotImplementedError
79
- end
80
-
81
- def add_namespace(element, prefix, uri)
82
- raise NotImplementedError
83
- end
84
-
85
- def namespace_prefix(namespace)
86
- raise NotImplementedError
87
- end
88
-
89
- def namespace_uri(namespace)
90
- raise NotImplementedError
91
- end
92
-
93
- # Attribute operations
94
- def attribute_value(attribute)
95
- raise NotImplementedError
96
- end
97
-
98
- def set_attribute_value(attribute, value)
99
- raise NotImplementedError
100
- end
101
-
102
- def attribute_namespace(attribute)
103
- raise NotImplementedError
104
- end
105
-
106
- # Text operations
107
- def create_text(document, content)
108
- raise NotImplementedError
109
- end
110
-
111
- def text_content(text)
112
- raise NotImplementedError
113
- end
114
-
115
- def set_text_content(text, content)
116
- raise NotImplementedError
117
- end
118
-
119
- # CDATA operations
120
- def create_cdata(document, content)
121
- raise NotImplementedError
122
- end
123
-
124
- # Comment operations
125
- def create_comment(document, content)
126
- raise NotImplementedError
127
- end
128
-
129
- def comment_content(comment)
130
- raise NotImplementedError
131
- end
132
-
133
- def set_comment_content(comment, content)
134
- raise NotImplementedError
135
- end
136
-
137
- # Processing instruction operations
138
- def create_processing_instruction(document, target, content)
139
- raise NotImplementedError
140
- end
141
-
142
- def processing_instruction_target(pi)
143
- raise NotImplementedError
144
- end
145
-
146
- def processing_instruction_content(pi)
147
- raise NotImplementedError
148
- end
149
-
150
- def set_processing_instruction_target(pi, target)
151
- raise NotImplementedError
152
- end
153
-
154
- def set_processing_instruction_content(pi, content)
155
- raise NotImplementedError
156
- end
157
-
158
- # Document specific operations
159
- def root(document)
160
- raise NotImplementedError
161
- end
162
-
163
- protected
164
-
165
- def normalize_options(options)
166
- {
167
- encoding: options[:encoding] || "UTF-8",
168
- indent: options[:indent] || 2,
169
- xml_declaration: options.fetch(:xml_declaration, true),
170
- pretty: options.fetch(:pretty, true),
171
- namespace_declarations: options.fetch(:namespace_declarations, true),
172
- }
5
+ module Moxml
6
+ module Adapter
7
+ AVALIABLE_ADAPTERS = %i[nokogiri oga].freeze # ox to be added later
8
+
9
+ class << self
10
+ def load(name)
11
+ require_adapter(name)
12
+ const_get(name.to_s.capitalize)
13
+ rescue LoadError => e
14
+ raise LoadError, "Could not load #{name} adapter. Please ensure the #{name} gem is available: #{e.message}"
15
+ end
16
+
17
+ private
18
+
19
+ def require_adapter(name)
20
+ require "#{__dir__}/adapter/#{name}"
21
+ rescue LoadError
22
+ begin
23
+ require name.to_s
24
+ require "#{__dir__}/adapter/#{name}"
25
+ rescue LoadError => e
26
+ raise LoadError, "Failed to load #{name} adapter: #{e.message}"
27
+ end
28
+ end
173
29
  end
174
30
  end
175
31
  end
@@ -1,30 +1,63 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Moxml
2
4
  class Attribute < Node
3
- def initialize(name_or_native = nil, value = nil)
4
- case name_or_native
5
- when String
6
- super(adapter.create_attribute(nil, name_or_native, value))
7
- else
8
- super(name_or_native)
9
- end
5
+ def name
6
+ @native.name
10
7
  end
11
8
 
12
- def name
13
- adapter.attribute_name(native)
9
+ def name=(new_name)
10
+ @native.name = new_name
14
11
  end
15
12
 
16
13
  def value
17
- adapter.attribute_value(native)
14
+ @native.value
18
15
  end
19
16
 
20
17
  def value=(new_value)
21
- adapter.set_attribute_value(native, new_value)
18
+ @native.value = normalize_xml_value(new_value)
19
+ end
20
+
21
+ def namespace
22
+ ns = adapter.namespace(@native)
23
+ ns && Namespace.new(ns, context)
24
+ end
25
+
26
+ def namespace=(ns)
27
+ adapter.set_namespace(@native, ns&.native)
28
+ end
29
+
30
+ def element
31
+ adapter.attribute_element(@native)
32
+ end
33
+
34
+ def remove
35
+ adapter.remove_attribute(element, name)
36
+ self
37
+ end
38
+
39
+ def ==(other)
40
+ return false unless other.is_a?(Attribute)
41
+
42
+ name == other.name && value == other.value && namespace == other.namespace
43
+ end
44
+
45
+ def to_s
46
+ if namespace&.prefix
47
+ "#{namespace.prefix}:#{name}=\"#{value}\""
48
+ else
49
+ "#{name}=\"#{value}\""
50
+ end
51
+ end
52
+
53
+ def attribute?
54
+ true
22
55
  end
23
56
 
24
- private
57
+ protected
25
58
 
26
- def create_native_node
27
- adapter.create_attribute(nil, "", "")
59
+ def adapter
60
+ context.config.adapter
28
61
  end
29
62
  end
30
63
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ class Builder
5
+ def initialize(context)
6
+ @context = context
7
+ @current = @document = context.create_document
8
+ @namespaces = {}
9
+ end
10
+
11
+ def build(&block)
12
+ instance_eval(&block)
13
+ @document
14
+ end
15
+
16
+ def declaration(version: "1.0", encoding: "UTF-8", standalone: nil)
17
+ @current.add_child(
18
+ @document.create_declaration(version, encoding, standalone)
19
+ )
20
+ end
21
+
22
+ def element(name, attributes = {}, &block)
23
+ el = @document.create_element(name)
24
+
25
+ attributes.each do |key, value|
26
+ el[key] = value
27
+ end
28
+
29
+ @current.add_child(el)
30
+
31
+ if block_given?
32
+ previous = @current
33
+ @current = el
34
+ instance_eval(&block)
35
+ @current = previous
36
+ end
37
+
38
+ el
39
+ end
40
+
41
+ def text(content)
42
+ @current.add_child(@document.create_text(content))
43
+ end
44
+
45
+ def cdata(content)
46
+ @current.add_child(@document.create_cdata(content))
47
+ end
48
+
49
+ def comment(content)
50
+ @current.add_child(@document.create_comment(content))
51
+ end
52
+
53
+ def processing_instruction(target, content)
54
+ @current.add_child(
55
+ @document.create_processing_instruction(target, content)
56
+ )
57
+ end
58
+
59
+ def namespace(prefix, uri)
60
+ @current.add_namespace(prefix, uri)
61
+ @namespaces[prefix] = uri
62
+ end
63
+ end
64
+ end
data/lib/moxml/cdata.rb CHANGED
@@ -1,39 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Moxml
2
4
  class Cdata < Node
3
- def initialize(content_or_native = nil)
4
- case content_or_native
5
- when String
6
- super(adapter.create_cdata(nil, content_or_native))
7
- else
8
- super(content_or_native)
9
- end
10
- end
11
-
12
5
  def content
13
- adapter.cdata_content(native)
6
+ adapter.cdata_content(@native)
14
7
  end
15
8
 
16
9
  def content=(text)
17
- adapter.set_cdata_content(native, text)
18
- self
19
- end
20
-
21
- def blank?
22
- content.strip.empty?
10
+ adapter.set_cdata_content(@native, normalize_xml_value(text))
23
11
  end
24
12
 
25
13
  def cdata?
26
14
  true
27
15
  end
28
-
29
- def text?
30
- false
31
- end
32
-
33
- private
34
-
35
- def create_native_node
36
- adapter.create_cdata(nil, "")
37
- end
38
16
  end
39
17
  end
data/lib/moxml/comment.rb CHANGED
@@ -1,35 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Moxml
2
4
  class Comment < Node
3
- def initialize(content_or_native = nil)
4
- case content_or_native
5
- when String
6
- super(adapter.create_comment(nil, content_or_native))
7
- else
8
- super(content_or_native)
9
- end
10
- end
11
-
12
5
  def content
13
- adapter.comment_content(native)
6
+ adapter.comment_content(@native)
14
7
  end
15
8
 
16
9
  def content=(text)
17
- adapter.set_comment_content(native, text)
18
- self
19
- end
20
-
21
- def blank?
22
- content.strip.empty?
10
+ text = normalize_xml_value(text)
11
+ adapter.validate_comment_content(text)
12
+ adapter.set_comment_content(@native, text)
23
13
  end
24
14
 
25
15
  def comment?
26
16
  true
27
17
  end
28
-
29
- private
30
-
31
- def create_native_node
32
- adapter.create_comment(nil, "")
33
- end
34
18
  end
35
19
  end
data/lib/moxml/config.rb CHANGED
@@ -1,23 +1,47 @@
1
- # lib/moxml/config.rb
1
+ # frozen_string_literal: true
2
+
2
3
  module Moxml
3
4
  class Config
4
- attr_accessor :backend, :huge_document,
5
+ VALID_ADAPTERS = %i[nokogiri oga ox].freeze
6
+ DEFAULT_ADAPTER = VALID_ADAPTERS.first
7
+
8
+ class << self
9
+ def default
10
+ @default ||= new(DEFAULT_ADAPTER, true, "UTF-8")
11
+ end
12
+ end
13
+
14
+ attr_reader :adapter_name
15
+ attr_accessor :strict_parsing,
5
16
  :default_encoding,
6
- :default_indent,
7
- :cdata_sections,
8
- :cdata_patterns,
9
- :strict_parsing,
10
- :entity_encoding
11
-
12
- def initialize
13
- @backend = :nokogiri
14
- @huge_document = false
15
- @default_encoding = "UTF-8"
17
+ :entity_encoding,
18
+ :default_indent
19
+
20
+ def initialize(adapter_name = nil, strict_parsing = nil, default_encoding = nil)
21
+ self.adapter = adapter_name || Config.default.adapter_name
22
+ @strict_parsing = strict_parsing || Config.default.strict_parsing
23
+ @default_encoding = default_encoding || Config.default.default_encoding
24
+ # reserved for future use
16
25
  @default_indent = 2
17
- @cdata_sections = true
18
- @cdata_patterns = ["script", "style"]
19
- @strict_parsing = true
20
26
  @entity_encoding = :basic
21
27
  end
28
+
29
+ def adapter=(name)
30
+ name = name.to_sym
31
+ @adapter = nil
32
+
33
+ unless VALID_ADAPTERS.include?(name)
34
+ raise ArgumentError, "Invalid adapter: #{name}. Valid adapters are: #{VALID_ADAPTERS.join(", ")}"
35
+ end
36
+
37
+ @adapter_name = name
38
+ adapter
39
+ end
40
+
41
+ alias default_adapter= adapter=
42
+
43
+ def adapter
44
+ @adapter ||= Adapter.load(@adapter_name)
45
+ end
22
46
  end
23
47
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ class Context
5
+ attr_reader :config
6
+
7
+ def initialize(adapter = nil)
8
+ @config = Config.new(adapter)
9
+ end
10
+
11
+ def create_document
12
+ Document.new(config.adapter.create_document, self)
13
+ end
14
+
15
+ def parse(xml, options = {})
16
+ config.adapter.parse(xml, default_options.merge(options))
17
+ end
18
+
19
+ private
20
+
21
+ def default_options
22
+ {
23
+ encoding: config.default_encoding,
24
+ strict: config.strict_parsing,
25
+ indent: config.default_indent
26
+ }
27
+ end
28
+ end
29
+ end
@@ -1,50 +1,40 @@
1
- # lib/moxml/declaration.rb
1
+ # frozen_string_literal: true
2
+
2
3
  module Moxml
3
4
  class Declaration < Node
4
- def initialize(version_or_native = "1.0", encoding = "UTF-8", standalone = nil)
5
- case version_or_native
6
- when String
7
- super(adapter.create_declaration(nil, version_or_native, encoding, standalone))
8
- else
9
- super(version_or_native)
10
- end
11
- end
5
+ ALLOWED_VERSIONS = ["1.0", "1.1"].freeze
6
+ ALLOWED_STANDALONE = %w[yes no].freeze
7
+ ALLOWED_ATTRIBUTES = %w[version encoding standalone].freeze
12
8
 
13
9
  def version
14
- adapter.declaration_version(native)
10
+ adapter.declaration_attribute(@native, "version")
15
11
  end
16
12
 
17
13
  def version=(new_version)
18
- adapter.set_declaration_version(native, new_version)
19
- self
14
+ adapter.validate_declaration_version(new_version)
15
+ adapter.set_declaration_attribute(@native, "version", new_version)
20
16
  end
21
17
 
22
18
  def encoding
23
- adapter.declaration_encoding(native)
19
+ adapter.declaration_attribute(@native, "encoding")
24
20
  end
25
21
 
26
22
  def encoding=(new_encoding)
27
- adapter.set_declaration_encoding(native, new_encoding)
28
- self
23
+ adapter.validate_declaration_encoding(new_encoding)
24
+ adapter.set_declaration_attribute(@native, "encoding", new_encoding)
29
25
  end
30
26
 
31
27
  def standalone
32
- adapter.declaration_standalone(native)
28
+ adapter.declaration_attribute(@native, "standalone")
33
29
  end
34
30
 
35
31
  def standalone=(new_standalone)
36
- adapter.set_declaration_standalone(native, new_standalone)
37
- self
32
+ adapter.validate_declaration_standalone(new_standalone)
33
+ adapter.set_declaration_attribute(@native, "standalone", new_standalone)
38
34
  end
39
35
 
40
- def to_xml
41
- adapter.serialize_declaration(native)
42
- end
43
-
44
- private
45
-
46
- def create_native_node
47
- adapter.create_declaration(nil, "1.0", "UTF-8", nil)
36
+ def declaration?
37
+ true
48
38
  end
49
39
  end
50
40
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ class Doctype < Node
5
+ def doctype?
6
+ true
7
+ end
8
+ end
9
+ end