uniword 1.6.1 → 1.7.0

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/uniword/builder/chart_builder.rb +129 -134
  3. data/lib/uniword/diff/package_differ.rb +16 -15
  4. data/lib/uniword/docx/package_integrity_checker.rb +10 -11
  5. data/lib/uniword/docx/reconciler/body.rb +1 -0
  6. data/lib/uniword/docx/reconciler/helpers.rb +27 -12
  7. data/lib/uniword/docx/reconciler/parts.rb +5 -0
  8. data/lib/uniword/infrastructure/mime_packager.rb +3 -10
  9. data/lib/uniword/infrastructure/mime_parser.rb +23 -1
  10. data/lib/uniword/math/plurimath_adapter.rb +28 -24
  11. data/lib/uniword/math_equation.rb +1 -1
  12. data/lib/uniword/mhtml/html_part.rb +19 -13
  13. data/lib/uniword/mhtml/math_converter.rb +18 -12
  14. data/lib/uniword/mhtml/mime_part.rb +5 -16
  15. data/lib/uniword/ooxml/element_order.rb +21 -0
  16. data/lib/uniword/ooxml/schema/element_serializer.rb +27 -35
  17. data/lib/uniword/streaming_parser.rb +22 -20
  18. data/lib/uniword/theme/theme_xml_parser.rb +3 -3
  19. data/lib/uniword/transformation/html_element_builder.rb +62 -48
  20. data/lib/uniword/transformation/html_formatting_mapper.rb +3 -3
  21. data/lib/uniword/transformation/html_to_ooxml_converter.rb +11 -11
  22. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -2
  23. data/lib/uniword/validation/schema_registry.rb +9 -16
  24. data/lib/uniword/validation/validators/xml_schema_validator.rb +75 -26
  25. data/lib/uniword/validation/verify_orchestrator.rb +1 -1
  26. data/lib/uniword/version.rb +1 -1
  27. data/lib/uniword/wp_drawing/position_h.rb +1 -1
  28. data/lib/uniword/wp_drawing/position_v.rb +1 -1
  29. data/lib/uniword/wp_drawing/size_rel_h.rb +1 -1
  30. data/lib/uniword/wp_drawing/size_rel_v.rb +1 -1
  31. data/lib/uniword.rb +4 -8
  32. metadata +8 -9
  33. data/lib/uniword/infrastructure/xml_namespace_normalizer.rb +0 -107
@@ -92,19 +92,12 @@ module Uniword
92
92
  # Empty line separating headers from body
93
93
  lines << ""
94
94
 
95
- # Body — encode if needed
95
+ # Body — encode if needed (part content is always decoded)
96
96
  encoding = part.content_transfer_encoding
97
97
  if encoding == "quoted-printable"
98
- # Use decoded_content to avoid double-encoding on round-trip
99
- body = quoted_printable_encode(part.decoded_content)
98
+ body = quoted_printable_encode(part.raw_content)
100
99
  elsif encoding == "base64"
101
- begin
102
- # Decoded content (round-trip: raw_content is already base64)
103
- body = Base64.strict_encode64(part.decoded_content)
104
- rescue ArgumentError
105
- # New part: raw_content is raw binary data
106
- body = Base64.strict_encode64(part.raw_content)
107
- end
100
+ body = Base64.strict_encode64(part.raw_content)
108
101
  body = body.gsub(/.{1,76}/, "\\0\r\n").rstrip
109
102
  else
110
103
  body = part.raw_content
@@ -96,11 +96,33 @@ module Uniword
96
96
  mime_part.content_location = content_location
97
97
  mime_part.content_transfer_encoding = encoding
98
98
  mime_part.content_id = content_id&.gsub(/[<>]/, "")
99
- mime_part.raw_content = body.rstrip
99
+ mime_part.decoded_content = decode_body(body.rstrip, encoding)
100
100
 
101
101
  mime_part
102
102
  end
103
103
 
104
+ # Decode a wire body into its content, per the declared
105
+ # Content-Transfer-Encoding. Stored content is always decoded.
106
+ # Tolerant of real-world corruption (Word truncates some base64
107
+ # tails), so base64 uses the non-strict decoder.
108
+ def decode_body(body, encoding)
109
+ case encoding&.downcase
110
+ when "base64"
111
+ Base64.decode64(body)
112
+ when "quoted-printable"
113
+ # Decode in BINARY so hex escapes map 1:1 to bytes; single-pass
114
+ # gsub, so an already-decoded byte is never re-decoded.
115
+ body.force_encoding("BINARY")
116
+ .gsub(/=\r?\n/, "")
117
+ .gsub(/=([0-9A-Fa-f]{2})/) do
118
+ [::Regexp.last_match(1).hex].pack("C")
119
+ end
120
+ .force_encoding("UTF-8")
121
+ else
122
+ body&.force_encoding("UTF-8") || body
123
+ end
124
+ end
125
+
104
126
  # Create a typed MimePart based on content type.
105
127
  def create_typed_part(content_type)
106
128
  return nil unless content_type
@@ -34,7 +34,7 @@ module Uniword
34
34
 
35
35
  # Create a MathEquation from OMML XML
36
36
  #
37
- # @param omml_xml [String, Nokogiri::XML::Node] OMML XML content
37
+ # @param omml_xml [String, Moxml::Node] OMML XML content
38
38
  # @return [MathEquation] New equation instance with parsed formula
39
39
  #
40
40
  # @example Parse OMML
@@ -94,7 +94,7 @@ module Uniword
94
94
 
95
95
  # Parse OMML to Plurimath formula
96
96
  #
97
- # @param node [Nokogiri::XML::Node] OMML node
97
+ # @param node [Moxml::Node] OMML node
98
98
  # @return [Plurimath::Math::Formula] Parsed formula
99
99
  #
100
100
  # @api private
@@ -131,7 +131,7 @@ module Uniword
131
131
 
132
132
  # Determine if equation is inline or block display
133
133
  #
134
- # @param node [Nokogiri::XML::Node] OMML node
134
+ # @param node [Moxml::Node] OMML node
135
135
  # @return [String] "inline" or "block"
136
136
  #
137
137
  # @api private
@@ -143,7 +143,7 @@ module Uniword
143
143
 
144
144
  # Extract equation properties from OMML
145
145
  #
146
- # @param node [Nokogiri::XML::Node] OMML node
146
+ # @param node [Moxml::Node] OMML node
147
147
  # @return [Hash] Extracted properties
148
148
  #
149
149
  # @api private
@@ -220,21 +220,26 @@ module Uniword
220
220
  #
221
221
  # @api private
222
222
  def self.wrap_block_equation(omml_content, equation)
223
- builder = Nokogiri::XML::Builder.new do |xml|
224
- xml["m"].oMathPara("xmlns:m" => OMML_NAMESPACE) do
225
- # Add paragraph properties if needed
226
- if equation.alignment || equation.break_enabled
227
- xml["m"].oMathParaPr do
228
- xml["m"].jc("m:val" => equation.alignment) if equation.alignment
229
- end
230
- end
223
+ doc = Moxml.new.create_document
224
+ para = doc.create_element("m:oMathPara")
225
+ para.add_namespace("m", OMML_NAMESPACE)
226
+ doc.root = para
231
227
 
232
- # Add the math content
233
- xml.parent << Nokogiri::XML.fragment(omml_content)
228
+ # Add paragraph properties if needed
229
+ if equation.alignment || equation.break_enabled
230
+ props = doc.create_element("m:oMathParaPr")
231
+ if equation.alignment
232
+ jc = doc.create_element("m:jc")
233
+ jc["m:val"] = equation.alignment
234
+ props.add_child(jc)
234
235
  end
236
+ para.add_child(props)
235
237
  end
236
238
 
237
- builder.to_xml
239
+ # Add the math content
240
+ para.add_child(Moxml.parse(omml_content).root)
241
+
242
+ doc.to_xml
238
243
  end
239
244
 
240
245
  # Format OMML output
@@ -248,33 +253,32 @@ module Uniword
248
253
  def self.format_omml(omml, options = {})
249
254
  return omml unless options[:pretty]
250
255
 
251
- doc = Nokogiri::XML(omml)
252
- doc.to_xml(indent: 2)
256
+ Moxml.parse(omml).to_xml(indent: 2)
253
257
  end
254
258
 
255
259
  # Parse XML node from string or node
256
260
  #
257
- # @param xml [String, Nokogiri::XML::Node] XML input
258
- # @return [Nokogiri::XML::Node] Parsed node
261
+ # @param xml [String, Moxml::Node] XML input
262
+ # @return [Moxml::Node] Parsed node
259
263
  #
260
264
  # @api private
261
265
  def self.parse_xml_node(xml)
262
266
  case xml
263
- when Nokogiri::XML::Node
267
+ when Moxml::Node
264
268
  xml
265
269
  when String
266
- doc = Nokogiri::XML(xml)
270
+ doc = Moxml.parse(xml)
267
271
  doc.root || raise(ArgumentError, "Invalid XML: no root element")
268
272
  else
269
273
  raise ArgumentError,
270
- "Expected String or Nokogiri::XML::Node, got #{xml.class}"
274
+ "Expected String or Moxml::Node, got #{xml.class}"
271
275
  end
272
276
  end
273
277
 
274
278
  # Extract the math node (m:oMath) from container
275
279
  #
276
- # @param node [Nokogiri::XML::Node] OMML node (could be oMath or oMathPara)
277
- # @return [Nokogiri::XML::Node] The m:oMath node
280
+ # @param node [Moxml::Node] OMML node (could be oMath or oMathPara)
281
+ # @return [Moxml::Node] The m:oMath node
278
282
  #
279
283
  # @api private
280
284
  def self.extract_math_node(node)
@@ -52,7 +52,7 @@ module Uniword
52
52
 
53
53
  # Create a MathEquation from OMML XML
54
54
  #
55
- # @param omml_xml [String, Nokogiri::XML::Node] OMML XML content
55
+ # @param omml_xml [String, Moxml::Node] OMML XML content
56
56
  # @return [MathEquation] New equation instance
57
57
  #
58
58
  # @example
@@ -7,26 +7,26 @@ module Uniword
7
7
  # Contains the Word HTML document with embedded XML metadata
8
8
  # (DocumentProperties, WordDocument settings, LatentStyles).
9
9
  class HtmlPart < MimePart
10
- # Parse the decoded HTML with Nokogiri
10
+ # Parse the decoded HTML with Moxml (Leptris HTML parser)
11
11
  def html_document
12
- @html_document ||= Nokogiri::HTML(decoded_content)
12
+ @html_document ||= Moxml.new.parse_html(decoded_content)
13
13
  end
14
14
 
15
15
  # Extract the <head> element as string
16
16
  def head_html
17
- node = html_document.at_css("head")
18
- node ? node.to_s : ""
17
+ node = html_document.at_xpath(".//head")
18
+ node ? node.to_xml : ""
19
19
  end
20
20
 
21
21
  # Extract the <body> element inner HTML
22
22
  def body_html
23
- node = html_document.at_css("body")
24
- node ? node.inner_html : ""
23
+ node = html_document.at_xpath(".//body")
24
+ node ? node.children.map(&:to_xml).join : ""
25
25
  end
26
26
 
27
27
  # Extract inline CSS styles from <style> tags
28
28
  def css_styles
29
- html_document.css("style").map(&:content).join("\n")
29
+ html_document.xpath(".//style").map(&:content).join("\n")
30
30
  end
31
31
 
32
32
  # Extract DocumentProperties XML from HTML head comments.
@@ -56,9 +56,15 @@ module Uniword
56
56
  "urn:schemas-microsoft-com:office:word", "w")
57
57
  end
58
58
 
59
+ # The document head element.
60
+ def head_element
61
+ html_document.at_xpath(".//head")
62
+ end
63
+
59
64
  # Extract all <xml> blocks from head
60
65
  def xml_blocks
61
- html_document.at_css("head")&.xpath("comment()")&.filter_map do |comment|
66
+ head = head_element
67
+ head&.xpath("comment()")&.filter_map do |comment|
62
68
  text = comment.text
63
69
  ::Regexp.last_match(1).strip if text =~ %r{<xml>(.*?)</xml>}m
64
70
  end || []
@@ -82,23 +88,23 @@ module Uniword
82
88
  # This method finds the requested root element and wraps it
83
89
  # with proper namespace declarations for lutaml-model.
84
90
  def extract_office_xml(element_name, namespace_uri, prefix)
85
- head = html_document.at_css("head")
91
+ head = head_element
86
92
  return nil unless head
87
93
 
88
94
  head.xpath("comment()").each do |comment|
89
95
  text = comment.text
90
- next unless text.include?("<#{prefix}:#{element_name}")
96
+ tag = "#{prefix}:#{element_name}"
97
+ next unless text.include?("<#{tag}")
91
98
 
92
99
  # Extract the specific element with its content
93
- pattern = %r{(<#{prefix}:#{element_name}[^>]*>.*?</#{prefix}:#{element_name}>)}m
100
+ pattern = %r{(<#{tag}[^>]*>.*?</#{tag}>)}m
94
101
  next unless text =~ pattern
95
102
 
96
103
  xml = ::Regexp.last_match(1)
97
104
  # Ensure namespace declaration is present
98
105
  ns_decl = "xmlns:#{prefix}=\"#{namespace_uri}\""
99
106
  unless xml.include?(ns_decl)
100
- xml = xml.sub(/(<#{prefix}:#{element_name})/,
101
- "\\1 #{ns_decl}")
107
+ xml = xml.sub(/(<#{tag})/, "\\1 #{ns_decl}")
102
108
  end
103
109
  return xml
104
110
  end
@@ -111,45 +111,51 @@ module Uniword
111
111
  #
112
112
  # Identifies math elements by tag name or special markers.
113
113
  #
114
- # @param element [Nokogiri::XML::Element] The HTML element
114
+ # @param element [Moxml::Element] The HTML element
115
115
  # @return [Boolean] true if element contains math content
116
116
  #
117
117
  # @example Check if element is math
118
118
  # is_math = MathConverter.math_element?(element)
119
119
  def self.math_element?(element)
120
- return false unless element.is_a?(Nokogiri::XML::Element)
120
+ return false unless element.is_a?(Moxml::Element)
121
121
 
122
122
  math_tags = %w[math mml:math m:oMath m:oMathPara]
123
123
  return true if math_tags.include?(element.name)
124
- return true if element["class"]&.include?("math")
125
- return true if element["data-mathml"]
126
124
 
127
- false
125
+ css_class = attribute_value(element, "class").to_s
126
+ return true if css_class.include?("math")
127
+
128
+ !attribute_value(element, "data-mathml").nil?
128
129
  end
129
130
 
130
131
  # Extract math content from element.
131
132
  #
132
133
  # Extracts the mathematical content from various element types.
133
134
  #
134
- # @param element [Nokogiri::XML::Element] The HTML element
135
+ # @param element [Moxml::Element] The HTML element
135
136
  # @return [Hash] Hash with :type and :content
136
137
  #
137
138
  # @example Extract math from element
138
139
  # math = MathConverter.extract_math(element)
139
140
  # # => { type: :mathml, content: "<math>...</math>" }
140
141
  def self.extract_math(element)
141
- if element["data-mathml"]
142
- { type: :mathml, content: element["data-mathml"] }
142
+ if (mathml = attribute_value(element, "data-mathml"))
143
+ { type: :mathml, content: mathml }
143
144
  elsif /^(math|mml:math)$/.match?(element.name)
144
- { type: :mathml, content: element.to_s }
145
+ { type: :mathml, content: element.to_xml }
145
146
  elsif /^m:(oMath|oMathPara)$/.match?(element.name)
146
- { type: :omml, content: element.to_s }
147
- elsif element["class"]&.include?("asciimath")
147
+ { type: :omml, content: element.to_xml }
148
+ elsif attribute_value(element, "class").to_s.include?("asciimath")
148
149
  { type: :asciimath, content: element.text }
149
150
  else
150
- { type: :unknown, content: element.to_s }
151
+ { type: :unknown, content: element.to_xml }
151
152
  end
152
153
  end
154
+
155
+ # Read an attribute value from a Moxml element.
156
+ def self.attribute_value(element, name)
157
+ element.attribute(name)&.value
158
+ end
153
159
  end
154
160
  end
155
161
  end
@@ -7,7 +7,10 @@ module Uniword
7
7
  # Base class for MIME parts in an MHTML document.
8
8
  #
9
9
  # Each MIME part has headers (Content-Type, Content-Location,
10
- # Content-Transfer-Encoding) and a body (raw content).
10
+ # Content-Transfer-Encoding) and a body (raw_content, always in
11
+ # DECODED form: the wire encoding declared by
12
+ # content_transfer_encoding is applied by MimePackager and removed
13
+ # by MimeParser, never here).
11
14
  #
12
15
  # Subclasses: HtmlPart, XmlPart, ImagePart, FileListPart, HeaderPart
13
16
  class MimePart < Lutaml::Model::Serializable
@@ -18,21 +21,7 @@ module Uniword
18
21
  attribute :raw_content, :string
19
22
 
20
23
  def decoded_content
21
- case content_transfer_encoding&.downcase
22
- when "base64"
23
- Base64.strict_decode64(raw_content.gsub(/\s+/, ""))
24
- when "quoted-printable"
25
- # Work in BINARY encoding to avoid gsub encoding conflicts,
26
- # then force UTF-8 at the end. Use .b to get BINARY encoding
27
- # without altering bytes.
28
- content = raw_content.dup.force_encoding("BINARY")
29
- content
30
- .gsub(/=\r?\n/, "")
31
- .gsub(/=([0-9A-Fa-f]{2})/) { [::Regexp.last_match(1).hex].pack("C") }
32
- .force_encoding("UTF-8")
33
- else
34
- raw_content
35
- end
24
+ raw_content
36
25
  end
37
26
 
38
27
  def decoded_content=(value)
@@ -77,6 +77,27 @@ module Uniword
77
77
  end
78
78
  module_function :insert_once
79
79
 
80
+ # Reposition an existing singleton entry after (or before) the
81
+ # given anchor. No-op when the entry or model order is absent.
82
+ # Falls back to end (after: missing) or start (before: missing).
83
+ #
84
+ # @param model [Lutaml::Model::Serializable] model to mutate
85
+ # @param name [String] element name to move
86
+ # @param after [String, nil] place after this element name
87
+ # @param before [String, nil] place before this element name
88
+ # @return [Array, nil] the mutated order
89
+ def move_once(model, name, after: nil, before: nil)
90
+ order = mutable_order(model)
91
+ return unless order
92
+
93
+ entry = order.find { |e| e.name == name }
94
+ return unless entry
95
+
96
+ order.delete_at(order.index { |e| e.name == name })
97
+ order.insert(insertion_index(order, after, before, nil), entry)
98
+ end
99
+ module_function :move_once
100
+
80
101
  # @return [Integer] index for the insert
81
102
  def insertion_index(order, after, before, position)
82
103
  if after
@@ -67,7 +67,7 @@ module Uniword
67
67
  schema_def = @schema.definition_for(element.class)
68
68
 
69
69
  # Build XML document
70
- doc = Nokogiri::XML::Document.new
70
+ doc = Moxml.new.create_document
71
71
 
72
72
  # Create root element with proper namespace
73
73
  root = serialize_element_to_node(doc, element, schema_def, options)
@@ -105,21 +105,21 @@ module Uniword
105
105
  OoxmlSchema.load(schema_path)
106
106
  end
107
107
 
108
- # Serialize element to Nokogiri node
108
+ # Serialize element to Moxml node
109
109
  #
110
- # @param doc [Nokogiri::XML::Document] XML document
110
+ # @param doc [Moxml::Document] XML document
111
111
  # @param element [Element] Element to serialize
112
112
  # @param schema_def [ElementDefinition] Schema definition
113
113
  # @param options [Hash] Serialization options
114
- # @return [Nokogiri::XML::Node] Created node
114
+ # @return [Moxml::Element] Created node
115
115
  def serialize_element_to_node(doc, element, schema_def, options)
116
116
  # Create element node with namespace
117
117
  prefix = schema_def.prefix
118
118
  local_name = schema_def.local_name
119
119
  namespace_uri = @schema.namespace[:uri]
120
120
 
121
- node = Nokogiri::XML::Node.new(local_name, doc)
122
- node.namespace = node.add_namespace_definition(prefix, namespace_uri)
121
+ node = doc.create_element(local_name)
122
+ node.add_namespace(prefix, namespace_uri)
123
123
 
124
124
  # Serialize attributes according to schema
125
125
  serialize_attributes_to_node(node, element, schema_def)
@@ -132,7 +132,7 @@ module Uniword
132
132
 
133
133
  # Serialize element attributes to node
134
134
  #
135
- # @param node [Nokogiri::XML::Node] Node to add attributes to
135
+ # @param node [Moxml::Element] Node to add attributes to
136
136
  # @param element [Element] Element with attributes
137
137
  # @param schema_def [ElementDefinition] Schema definition
138
138
  def serialize_attributes_to_node(node, element, schema_def)
@@ -157,8 +157,8 @@ module Uniword
157
157
 
158
158
  # Serialize element children to node
159
159
  #
160
- # @param doc [Nokogiri::XML::Document] XML document
161
- # @param node [Nokogiri::XML::Node] Parent node
160
+ # @param doc [Moxml::Document] XML document
161
+ # @param node [Moxml::Element] Parent node
162
162
  # @param element [Element] Element with children
163
163
  # @param schema_def [ElementDefinition] Schema definition
164
164
  # @param options [Hash] Serialization options
@@ -190,11 +190,11 @@ module Uniword
190
190
 
191
191
  # Serialize a single child element to node
192
192
  #
193
- # @param doc [Nokogiri::XML::Document] XML document
193
+ # @param doc [Moxml::Document] XML document
194
194
  # @param child [Element, Object] Child element or value
195
195
  # @param child_def [ChildDefinition] Child schema definition
196
196
  # @param options [Hash] Serialization options
197
- # @return [Nokogiri::XML::Node, nil] Created node
197
+ # @return [Moxml::Element, nil] Created node
198
198
  def serialize_child_to_node(doc, child, child_def, options)
199
199
  # Get namespace prefix from tag (e.g., 'w:p' -> 'w')
200
200
  tag_parts = child_def.tag.split(":")
@@ -204,24 +204,21 @@ module Uniword
204
204
 
205
205
  if child_def.presence_only?
206
206
  # Presence-only element (e.g., <w:b/> for bold)
207
- node = Nokogiri::XML::Node.new(local_name, doc)
208
- node.namespace = node.add_namespace_definition(prefix,
209
- namespace_uri)
207
+ node = doc.create_element(local_name)
208
+ node.add_namespace(prefix, namespace_uri)
210
209
  node
211
210
  elsif child.is_a?(String)
212
211
  # Simple string value (e.g., text content)
213
- node = Nokogiri::XML::Node.new(local_name, doc)
214
- node.namespace = node.add_namespace_definition(prefix,
215
- namespace_uri)
212
+ node = doc.create_element(local_name)
213
+ node.add_namespace(prefix, namespace_uri)
216
214
  node.content = child
217
215
  node
218
216
  elsif child.is_a?(Uniword::Element)
219
217
  # Check if this is a TextElement that needs special handling
220
218
  if child.instance_of?(::Uniword::TextElement) && child.is_a?(Uniword::TextElement)
221
219
  # TextElement: serialize as element with text content
222
- node = Nokogiri::XML::Node.new(local_name, doc)
223
- node.namespace = node.add_namespace_definition(prefix,
224
- namespace_uri)
220
+ node = doc.create_element(local_name)
221
+ node.add_namespace(prefix, namespace_uri)
225
222
  node.content = child.content.to_s if child.content
226
223
  node
227
224
  else
@@ -238,9 +235,8 @@ module Uniword
238
235
  else
239
236
  # For properties without schema, create empty element
240
237
  # (will be filled with children if schema definition exists for element type)
241
- node = Nokogiri::XML::Node.new(local_name, doc)
242
- node.namespace = node.add_namespace_definition(prefix,
243
- namespace_uri)
238
+ node = doc.create_element(local_name)
239
+ node.add_namespace(prefix, namespace_uri)
244
240
 
245
241
  if child_def.has_attributes?
246
242
  child_def.attributes.each do |attr_config|
@@ -256,9 +252,8 @@ module Uniword
256
252
  end
257
253
  elsif child_def.has_attributes?
258
254
  # Element with attributes but no child elements
259
- node = Nokogiri::XML::Node.new(local_name, doc)
260
- node.namespace = node.add_namespace_definition(prefix,
261
- namespace_uri)
255
+ node = doc.create_element(local_name)
256
+ node.add_namespace(prefix, namespace_uri)
262
257
 
263
258
  child_def.attributes.each do |attr_config|
264
259
  attr_def = AttributeDefinition.new(attr_config)
@@ -271,23 +266,20 @@ module Uniword
271
266
  node
272
267
  elsif child.is_a?(String)
273
268
  # Simple string value
274
- node = Nokogiri::XML::Node.new(local_name, doc)
275
- node.namespace = node.add_namespace_definition(prefix,
276
- namespace_uri)
269
+ node = doc.create_element(local_name)
270
+ node.add_namespace(prefix, namespace_uri)
277
271
  node.content = child
278
272
  node
279
273
  elsif child.is_a?(Uniword::TextElement)
280
274
  # TextElement or similar - get the content
281
- node = Nokogiri::XML::Node.new(local_name, doc)
282
- node.namespace = node.add_namespace_definition(prefix,
283
- namespace_uri)
275
+ node = doc.create_element(local_name)
276
+ node.add_namespace(prefix, namespace_uri)
284
277
  node.content = child.content.to_s if child.content
285
278
  node
286
279
  elsif child
287
280
  # Other value element - convert to string
288
- node = Nokogiri::XML::Node.new(local_name, doc)
289
- node.namespace = node.add_namespace_definition(prefix,
290
- namespace_uri)
281
+ node = doc.create_element(local_name)
282
+ node.add_namespace(prefix, namespace_uri)
291
283
  node.content = child.to_s
292
284
  node
293
285
  end