moxml 0.1.0 → 0.1.2

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 +401 -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 +319 -0
  14. data/lib/moxml/adapter/oga.rb +318 -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 +63 -97
  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 +130 -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
@@ -1,99 +1,87 @@
1
+ # frozen_string_literal: true
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
+
1
13
  module Moxml
2
14
  class Document < Node
3
- def self.parse(input, options = {})
4
- new(Moxml.adapter.parse(input, options))
15
+ def root=(element)
16
+ adapter.set_root(@native, element.native)
5
17
  end
6
18
 
7
19
  def root
8
- wrap_node(adapter.root(native))
20
+ root_element = adapter.root(@native)
21
+ root_element ? Element.wrap(root_element, context) : nil
9
22
  end
10
23
 
11
24
  def create_element(name)
12
- Element.new(adapter.create_element(native, name))
25
+ Element.new(adapter.create_element(name), context)
13
26
  end
14
27
 
15
28
  def create_text(content)
16
- Text.new(adapter.create_text(native, content))
29
+ Text.new(adapter.create_text(content), context)
17
30
  end
18
31
 
19
32
  def create_cdata(content)
20
- Cdata.new(adapter.create_cdata(native, content))
33
+ Cdata.new(adapter.create_cdata(content), context)
21
34
  end
22
35
 
23
36
  def create_comment(content)
24
- Comment.new(adapter.create_comment(native, content))
37
+ Comment.new(adapter.create_comment(content), context)
25
38
  end
26
39
 
27
- def create_processing_instruction(target, content)
28
- ProcessingInstruction.new(
29
- adapter.create_processing_instruction(native, target, content)
40
+ def create_doctype(name, external_id, system_id)
41
+ Doctype.new(
42
+ adapter.create_doctype(name, external_id, system_id),
43
+ context
30
44
  )
31
45
  end
32
46
 
33
- def to_xml(options = {})
34
- adapter.serialize(native, options)
35
- end
36
-
37
- def encoding
38
- declaration&.encoding
39
- end
40
-
41
- def encoding=(encoding)
42
- (declaration || add_declaration).encoding = encoding
43
- end
44
-
45
- def version
46
- declaration&.version
47
- end
48
-
49
- def version=(version)
50
- (declaration || add_declaration).version = version
51
- end
52
-
53
- def standalone
54
- declaration&.standalone
47
+ def create_processing_instruction(target, content)
48
+ ProcessingInstruction.new(
49
+ adapter.create_processing_instruction(target, content),
50
+ context
51
+ )
55
52
  end
56
53
 
57
- def standalone=(standalone)
58
- (declaration || add_declaration).standalone = standalone
54
+ def create_declaration(version = "1.0", encoding = "UTF-8", standalone = nil)
55
+ decl = adapter.create_declaration(version, encoding, standalone)
56
+ Declaration.new(decl, context)
59
57
  end
60
58
 
61
- def declaration
62
- children.find { |node| node.is_a?(Declaration) }
63
- end
59
+ def add_child(node)
60
+ node = prepare_node(node)
64
61
 
65
- def add_declaration(version = "1.0", encoding = "UTF-8", standalone = nil)
66
- decl = Declaration.new(version, encoding, standalone)
67
- if declaration
68
- declaration.replace(decl)
62
+ if node.is_a?(Declaration)
63
+ if children.empty?
64
+ adapter.add_child(@native, node.native)
65
+ else
66
+ adapter.add_previous_sibling(children.first.native, node.native)
67
+ end
68
+ elsif root && !node.is_a?(ProcessingInstruction) && !node.is_a?(Comment)
69
+ raise Error, "Document already has a root element"
69
70
  else
70
- add_child(decl)
71
+ adapter.add_child(@native, node.native)
71
72
  end
72
- decl
73
- end
74
-
75
- def css(selector)
76
- NodeSet.new(adapter.css(native, selector))
73
+ self
77
74
  end
78
75
 
79
- def xpath(expression, namespaces = {})
80
- NodeSet.new(adapter.xpath(native, expression, namespaces))
76
+ def xpath(expression, namespaces = nil)
77
+ native_nodes = adapter.xpath(@native, expression, namespaces)
78
+ NodeSet.new(native_nodes, context)
81
79
  end
82
80
 
83
- def at_css(selector)
84
- node = adapter.at_css(native, selector)
85
- node.nil? ? nil : wrap_node(node)
86
- end
87
-
88
- def at_xpath(expression, namespaces = {})
89
- node = adapter.at_xpath(native, expression, namespaces)
90
- node.nil? ? nil : wrap_node(node)
91
- end
92
-
93
- private
94
-
95
- def create_native_node
96
- adapter.create_document
81
+ def at_xpath(expression, namespaces = nil)
82
+ if (native_node = adapter.at_xpath(@native, expression, namespaces))
83
+ Node.wrap(native_node, context)
84
+ end
97
85
  end
98
86
  end
99
87
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ class DocumentBuilder
5
+ attr_reader :context
6
+
7
+ def initialize(context)
8
+ @context = context
9
+ @node_stack = []
10
+ end
11
+
12
+ def build(native_doc)
13
+ @current_doc = context.create_document
14
+ visit_node(native_doc)
15
+ @current_doc
16
+ end
17
+
18
+ private
19
+
20
+ def visit_node(node)
21
+ node.respond_to?(:name) ? node.name : node
22
+ method_name = "visit_#{node_type(node)}"
23
+ return unless respond_to?(method_name, true)
24
+
25
+ send(method_name, node)
26
+ end
27
+
28
+ def visit_document(doc)
29
+ @node_stack.push(@current_doc)
30
+ visit_children(doc)
31
+ @node_stack.pop
32
+ end
33
+
34
+ def visit_element(node)
35
+ element = Element.new(node, context)
36
+ if @node_stack.empty?
37
+ # For root element, we need to set it directly
38
+ adapter.set_root(@current_doc.native, element.native)
39
+ else
40
+ @node_stack.last.add_child(element)
41
+ end
42
+ @node_stack.push(element)
43
+ visit_children(node)
44
+ @node_stack.pop
45
+ element
46
+ end
47
+
48
+ def visit_text(node)
49
+ @node_stack.last.add_child(Text.new(node, context)) if @node_stack.any?
50
+ end
51
+
52
+ def visit_cdata(node)
53
+ @node_stack.last.add_child(Cdata.new(node, context)) if @node_stack.any?
54
+ end
55
+
56
+ def visit_comment(node)
57
+ @node_stack.last.add_child(Comment.new(node, context)) if @node_stack.any?
58
+ end
59
+
60
+ def visit_processing_instruction(node)
61
+ @node_stack.last.add_child(ProcessingInstruction.new(node, context)) if @node_stack.any?
62
+ end
63
+
64
+ def visit_doctype(node)
65
+ @node_stack.last.add_child(Doctype.new(node, context)) if @node_stack.any?
66
+ end
67
+
68
+ def visit_children(node)
69
+ node_children = children(node).dup
70
+ node_children.each do |child|
71
+ visit_node(child)
72
+ end
73
+ end
74
+
75
+ def node_type(node)
76
+ context.config.adapter.node_type(node)
77
+ end
78
+
79
+ def children(node)
80
+ context.config.adapter.children(node)
81
+ end
82
+
83
+ def adapter
84
+ context.config.adapter
85
+ end
86
+ end
87
+ end
data/lib/moxml/element.rb CHANGED
@@ -1,145 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute"
4
+ require_relative "namespace"
5
+
1
6
  module Moxml
2
7
  class Element < Node
3
- def initialize(name_or_native = nil)
4
- case name_or_native
5
- when String
6
- super(adapter.create_element(nil, name_or_native))
7
- else
8
- super(name_or_native)
9
- end
10
- end
11
-
12
8
  def name
13
- adapter.node_name(native)
9
+ adapter.node_name(@native)
14
10
  end
15
11
 
16
- def attributes
17
- adapter.attributes(native).transform_values { |attr| Attribute.new(attr) }
12
+ def name=(value)
13
+ adapter.set_node_name(@native, value)
18
14
  end
19
15
 
20
16
  def []=(name, value)
21
- adapter.set_attribute(native, name, value)
17
+ adapter.set_attribute(@native, name, normalize_xml_value(value))
22
18
  end
23
19
 
24
20
  def [](name)
25
- attr = adapter.get_attribute(native, name)
26
- attr.nil? ? nil : Attribute.new(attr)
21
+ adapter.get_attribute_value(@native, name)
27
22
  end
28
23
 
29
- def add_child(node)
30
- adapter.add_child(native, node.native)
31
- self
24
+ def attribute(name)
25
+ native_attr = adapter.get_attribute(@native, name)
26
+ native_attr && Attribute.new(native_attr, context)
32
27
  end
33
28
 
34
- def namespace
35
- ns = adapter.namespace(native)
36
- ns.nil? ? nil : Namespace.new(ns)
29
+ def attributes
30
+ adapter.attributes(@native).map do |attr|
31
+ Attribute.new(attr, context)
32
+ end
37
33
  end
38
34
 
39
- def namespace=(ns)
40
- adapter.set_namespace(native, ns&.native)
35
+ def remove_attribute(name)
36
+ adapter.remove_attribute(@native, name)
41
37
  self
42
38
  end
43
39
 
44
- def namespaces
45
- adapter.namespaces(native).transform_values { |ns| Namespace.new(ns) }
46
- end
47
-
48
- def css(selector)
49
- NodeSet.new(adapter.css(native, selector))
50
- end
51
-
52
- def xpath(expression, namespaces = {})
53
- NodeSet.new(adapter.xpath(native, expression, namespaces))
54
- end
55
-
56
- def at_css(selector)
57
- node = adapter.at_css(native, selector)
58
- node.nil? ? nil : wrap_node(node)
59
- end
60
-
61
- def at_xpath(expression, namespaces = {})
62
- node = adapter.at_xpath(native, expression, namespaces)
63
- node.nil? ? nil : wrap_node(node)
64
- end
65
-
66
- def blank?
67
- text.strip.empty? && children.empty?
68
- end
69
-
70
- def value
71
- text.strip
72
- end
73
-
74
- def value=(val)
75
- self.text = val.to_s
40
+ def add_namespace(prefix, uri)
41
+ validate_uri(uri)
42
+ adapter.create_native_namespace(@native, prefix, uri)
43
+ self
44
+ rescue ValidationError => e
45
+ raise Moxml::NamespaceError, e.message
76
46
  end
47
+ alias add_namespace_definition add_namespace
77
48
 
78
- def matches?(selector)
79
- adapter.matches?(native, selector)
49
+ # it's NOT the same as namespaces.first
50
+ def namespace
51
+ ns = adapter.namespace(@native)
52
+ ns && Namespace.new(ns, context)
80
53
  end
81
54
 
82
- def ancestors
83
- NodeSet.new(adapter.ancestors(native))
55
+ # it does NOT change the list of namespace definitions
56
+ def namespace=(ns_or_hash)
57
+ if ns_or_hash.is_a?(Hash)
58
+ adapter.set_namespace(
59
+ @native,
60
+ adapter.create_namespace(@native, *ns_or_hash.to_a.first)
61
+ )
62
+ else
63
+ adapter.set_namespace(@native, ns_or_hash&.native)
64
+ end
84
65
  end
85
66
 
86
- def descendants
87
- NodeSet.new(adapter.descendants(native))
67
+ def namespaces
68
+ adapter.namespace_definitions(@native).map do |ns|
69
+ Namespace.new(ns, context)
70
+ end
88
71
  end
72
+ alias namespace_definitions namespaces
89
73
 
90
- def previous_elements
91
- NodeSet.new(adapter.previous_elements(native))
74
+ def text
75
+ adapter.text_content(@native)
92
76
  end
93
77
 
94
- def next_elements
95
- NodeSet.new(adapter.next_elements(native))
78
+ def text=(content)
79
+ adapter.set_text_content(@native, normalize_xml_value(content))
96
80
  end
97
81
 
98
82
  def inner_text
99
- adapter.inner_text(native)
100
- end
101
-
102
- def inner_text=(text)
103
- adapter.set_inner_text(native, text)
104
- self
83
+ adapter.inner_text(@native)
105
84
  end
106
85
 
107
- def key?(name)
108
- adapter.has_attribute?(native, name)
86
+ def inner_html
87
+ adapter.inner_html(@native)
109
88
  end
110
89
 
111
- alias has_attribute? key?
112
-
113
- def classes
114
- (self["class"] || "").split(/\s+/)
90
+ def inner_html=(html)
91
+ doc = context.parse("<root>#{html}</root>")
92
+ adapter.replace_children(@native, doc.root.children.map(&:native))
115
93
  end
116
94
 
117
- def add_class(*names)
118
- self["class"] = (classes + names).uniq.join(" ")
95
+ # Fluent interface methods
96
+ def with_attribute(name, value)
97
+ self[name] = value
119
98
  self
120
99
  end
121
100
 
122
- def remove_class(*names)
123
- self["class"] = (classes - names).join(" ")
101
+ def with_namespace(prefix, uri)
102
+ add_namespace(prefix, uri)
124
103
  self
125
104
  end
126
105
 
127
- def toggle_class(name)
128
- if classes.include?(name)
129
- remove_class(name)
130
- else
131
- add_class(name)
132
- end
133
- end
134
-
135
- def has_class?(name)
136
- classes.include?(name)
137
- end
138
-
139
- private
140
-
141
- def create_native_node
142
- adapter.create_element(nil, "")
106
+ def with_text(content)
107
+ self.text = content
108
+ self
143
109
  end
144
110
  end
145
111
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ class Error < StandardError; end
5
+
6
+ class ParseError < Error
7
+ attr_reader :line, :column
8
+
9
+ def initialize(message, line: nil, column: nil)
10
+ @line = line
11
+ @column = column
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ class ValidationError < Error; end
17
+ class XPathError < Error; end
18
+ class NamespaceError < Error; end
19
+ class AdapterError < Error; end
20
+ end
@@ -1,54 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Moxml
2
4
  class Namespace < Node
3
- def initialize(prefix_or_native = nil, uri = nil)
4
- case prefix_or_native
5
- when String
6
- super(adapter.create_namespace(nil, prefix_or_native, uri))
7
- else
8
- super(prefix_or_native)
9
- end
10
- end
11
-
12
5
  def prefix
13
- adapter.namespace_prefix(native)
14
- end
15
-
16
- def prefix=(new_prefix)
17
- adapter.set_namespace_prefix(native, new_prefix)
18
- self
6
+ adapter.namespace_prefix(@native)
19
7
  end
20
8
 
21
9
  def uri
22
- adapter.namespace_uri(native)
23
- end
24
-
25
- def uri=(new_uri)
26
- adapter.set_namespace_uri(native, new_uri)
27
- self
28
- end
29
-
30
- def blank?
31
- uri.nil? || uri.empty?
32
- end
33
-
34
- def namespace?
35
- true
10
+ adapter.namespace_uri(@native)
36
11
  end
37
12
 
38
13
  def ==(other)
39
- other.is_a?(Namespace) &&
40
- other.prefix == prefix &&
41
- other.uri == uri
14
+ other.is_a?(Namespace) && prefix == other.prefix && uri == other.uri
42
15
  end
43
16
 
44
17
  def to_s
45
- prefix ? "xmlns:#{prefix}='#{uri}'" : "xmlns='#{uri}'"
18
+ if prefix
19
+ %(xmlns:#{prefix}="#{uri}")
20
+ else
21
+ %(xmlns="#{uri}")
22
+ end
46
23
  end
47
24
 
48
- private
49
-
50
- def create_native_node
51
- adapter.create_namespace(nil, "", "")
25
+ def namespace?
26
+ true
52
27
  end
53
28
  end
54
29
  end