moxml 0.1.0 → 0.1.1

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 (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
@@ -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,107 @@
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
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
72
46
  end
47
+ alias add_namespace_definition add_namespace
73
48
 
74
- def value=(val)
75
- self.text = val.to_s
49
+ # it's NOT the same as namespaces.first
50
+ def namespace
51
+ ns = adapter.namespace(@native)
52
+ ns && Namespace.new(ns, context)
76
53
  end
77
54
 
78
- def matches?(selector)
79
- adapter.matches?(native, selector)
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
80
65
  end
81
66
 
82
- def ancestors
83
- NodeSet.new(adapter.ancestors(native))
67
+ def namespaces
68
+ adapter.namespace_definitions(@native).map do |ns|
69
+ Namespace.new(ns, context)
70
+ end
84
71
  end
72
+ alias namespace_definitions namespaces
85
73
 
86
- def descendants
87
- NodeSet.new(adapter.descendants(native))
74
+ def text
75
+ adapter.text_content(@native)
88
76
  end
89
77
 
90
- def previous_elements
91
- NodeSet.new(adapter.previous_elements(native))
78
+ def text=(content)
79
+ adapter.set_text_content(@native, normalize_xml_value(content))
92
80
  end
93
81
 
94
- def next_elements
95
- NodeSet.new(adapter.next_elements(native))
82
+ def inner_html
83
+ adapter.inner_html(@native)
96
84
  end
97
85
 
98
- def inner_text
99
- adapter.inner_text(native)
86
+ def inner_html=(html)
87
+ doc = context.parse("<root>#{html}</root>")
88
+ adapter.replace_children(@native, doc.root.children.map(&:native))
100
89
  end
101
90
 
102
- def inner_text=(text)
103
- adapter.set_inner_text(native, text)
91
+ # Fluent interface methods
92
+ def with_attribute(name, value)
93
+ self[name] = value
104
94
  self
105
95
  end
106
96
 
107
- def key?(name)
108
- adapter.has_attribute?(native, name)
109
- end
110
-
111
- alias has_attribute? key?
112
-
113
- def classes
114
- (self["class"] || "").split(/\s+/)
115
- end
116
-
117
- def add_class(*names)
118
- self["class"] = (classes + names).uniq.join(" ")
97
+ def with_namespace(prefix, uri)
98
+ add_namespace(prefix, uri)
119
99
  self
120
100
  end
121
101
 
122
- def remove_class(*names)
123
- self["class"] = (classes - names).join(" ")
102
+ def with_text(content)
103
+ self.text = content
124
104
  self
125
105
  end
126
-
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, "")
143
- end
144
106
  end
145
107
  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