uniword 1.6.0 → 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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/exe/uniword +9 -0
  3. data/lib/uniword/accessibility/accessibility_report.rb +8 -0
  4. data/lib/uniword/builder/chart_builder.rb +129 -134
  5. data/lib/uniword/cli/completions.rb +99 -0
  6. data/lib/uniword/cli/generate_cli.rb +5 -0
  7. data/lib/uniword/cli/help_switch.rb +45 -0
  8. data/lib/uniword/cli/helpers.rb +1 -0
  9. data/lib/uniword/cli/main.rb +118 -21
  10. data/lib/uniword/cli/no_color.rb +21 -0
  11. data/lib/uniword/diff/package_differ.rb +16 -15
  12. data/lib/uniword/document_factory.rb +2 -2
  13. data/lib/uniword/document_writer.rb +32 -10
  14. data/lib/uniword/docx/package_integrity_checker.rb +10 -11
  15. data/lib/uniword/docx/reconciler/body.rb +1 -0
  16. data/lib/uniword/docx/reconciler/helpers.rb +27 -12
  17. data/lib/uniword/docx/reconciler/parts.rb +5 -0
  18. data/lib/uniword/find_replace/paragraph_walker.rb +49 -12
  19. data/lib/uniword/find_replace/scope.rb +7 -4
  20. data/lib/uniword/infrastructure/mime_packager.rb +3 -10
  21. data/lib/uniword/infrastructure/mime_parser.rb +23 -1
  22. data/lib/uniword/math/plurimath_adapter.rb +28 -24
  23. data/lib/uniword/math_equation.rb +1 -1
  24. data/lib/uniword/mhtml/html_part.rb +19 -13
  25. data/lib/uniword/mhtml/math_converter.rb +18 -12
  26. data/lib/uniword/mhtml/mime_part.rb +5 -16
  27. data/lib/uniword/mhtml/xml_part.rb +3 -3
  28. data/lib/uniword/ooxml/additional_characteristics.rb +4 -4
  29. data/lib/uniword/ooxml/element_order.rb +21 -0
  30. data/lib/uniword/ooxml/schema/element_serializer.rb +27 -35
  31. data/lib/uniword/ooxml/schema_library.rb +2 -2
  32. data/lib/uniword/ooxml/types/characteristics_val.rb +16 -0
  33. data/lib/uniword/ooxml/types/schema_library_val.rb +17 -0
  34. data/lib/uniword/ooxml/types/variant_types.rb +10 -2
  35. data/lib/uniword/ooxml/types/vt_attr.rb +19 -0
  36. data/lib/uniword/ooxml/types/wml_int.rb +17 -0
  37. data/lib/uniword/ooxml/types/wml_val.rb +19 -0
  38. data/lib/uniword/ooxml/types.rb +12 -0
  39. data/lib/uniword/properties/alignment.rb +4 -0
  40. data/lib/uniword/properties/boolean_element_factory.rb +1 -1
  41. data/lib/uniword/properties/font_size.rb +3 -0
  42. data/lib/uniword/review/review_manager.rb +34 -5
  43. data/lib/uniword/review/revision_resolver.rb +190 -0
  44. data/lib/uniword/review.rb +1 -0
  45. data/lib/uniword/revision.rb +3 -3
  46. data/lib/uniword/serialization/ooxml_serializer.rb +3 -3
  47. data/lib/uniword/streaming_parser.rb +22 -20
  48. data/lib/uniword/theme/theme_xml_parser.rb +4 -6
  49. data/lib/uniword/transformation/html_element_builder.rb +62 -48
  50. data/lib/uniword/transformation/html_formatting_mapper.rb +3 -3
  51. data/lib/uniword/transformation/html_to_ooxml_converter.rb +11 -11
  52. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -2
  53. data/lib/uniword/validation/opc_validator.rb +6 -7
  54. data/lib/uniword/validation/rules/document_context.rb +3 -3
  55. data/lib/uniword/validation/rules/images_rule.rb +1 -1
  56. data/lib/uniword/validation/rules/theme_rule.rb +2 -2
  57. data/lib/uniword/validation/schema_registry.rb +10 -17
  58. data/lib/uniword/validation/validators/xml_schema_validator.rb +75 -26
  59. data/lib/uniword/validation/verify_orchestrator.rb +1 -1
  60. data/lib/uniword/version.rb +1 -1
  61. data/lib/uniword/wordprocessingml/deletion.rb +41 -0
  62. data/lib/uniword/wordprocessingml/insertion.rb +41 -0
  63. data/lib/uniword/wordprocessingml/page_margins.rb +7 -7
  64. data/lib/uniword/wordprocessingml/paragraph.rb +30 -4
  65. data/lib/uniword/wordprocessingml/settings.rb +3 -4
  66. data/lib/uniword/wordprocessingml/style_cleanup.rb +5 -14
  67. data/lib/uniword/wordprocessingml/table_borders.rb +1 -1
  68. data/lib/uniword/wordprocessingml/table_cell_borders.rb +1 -1
  69. data/lib/uniword/wordprocessingml/text.rb +0 -1
  70. data/lib/uniword/wordprocessingml.rb +5 -0
  71. data/lib/uniword/wp_drawing/position_h.rb +1 -1
  72. data/lib/uniword/wp_drawing/position_v.rb +1 -1
  73. data/lib/uniword/wp_drawing/size_rel_h.rb +1 -1
  74. data/lib/uniword/wp_drawing/size_rel_v.rb +1 -1
  75. data/lib/uniword.rb +9 -7
  76. metadata +19 -15
  77. data/lib/uniword/infrastructure/xml_namespace_normalizer.rb +0 -107
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
4
-
5
3
  module Uniword
6
4
  # Streaming parser for large DOCX documents.
7
5
  #
@@ -48,21 +46,20 @@ module Uniword
48
46
  # @param zip_content [Hash] Extracted ZIP content from DOCX
49
47
  # @return [Document] The parsed document
50
48
  def parse_streaming(zip_content)
51
- document = Document.new
49
+ document = Wordprocessingml::DocumentRoot.new
52
50
  document_xml = zip_content["word/document.xml"]
53
51
 
54
52
  return document unless document_xml
55
53
 
56
- # Use Nokogiri's SAX parser for streaming
54
+ # Stream via Moxml's SAX parser
57
55
  handler = DocumentSaxHandler.new(document, @paragraph_limit, @table_limit)
58
- parser = Nokogiri::XML::SAX::Parser.new(handler)
59
- parser.parse(document_xml)
56
+ Moxml.new.config.adapter.sax_parse(document_xml, handler)
60
57
 
61
58
  document
62
59
  end
63
60
 
64
61
  # SAX handler for streaming document parsing
65
- class DocumentSaxHandler < Nokogiri::XML::SAX::Document
62
+ class DocumentSaxHandler < Moxml::SAX::Handler
66
63
  attr_reader :document
67
64
 
68
65
  def initialize(document, paragraph_limit = nil, table_limit = nil)
@@ -86,8 +83,8 @@ module Uniword
86
83
  end
87
84
 
88
85
  # Called when an element starts
89
- def start_element(name, attributes = [])
90
- attrs = attributes.to_h
86
+ def on_start_element(name, _attributes = {}, _namespaces = {})
87
+ name = local_name(name)
91
88
  @element_stack.push(name)
92
89
 
93
90
  case name
@@ -96,7 +93,7 @@ module Uniword
96
93
  when "r"
97
94
  start_run
98
95
  when "t"
99
- start_text(attrs)
96
+ start_text
100
97
  when "tbl"
101
98
  start_table
102
99
  when "tr"
@@ -109,7 +106,8 @@ module Uniword
109
106
  end
110
107
 
111
108
  # Called when an element ends
112
- def end_element(name)
109
+ def on_end_element(name)
110
+ name = local_name(name)
113
111
  @element_stack.pop
114
112
 
115
113
  case name
@@ -131,7 +129,7 @@ module Uniword
131
129
  end
132
130
 
133
131
  # Called when text content is encountered
134
- def characters(string)
132
+ def on_characters(string)
135
133
  return unless @current_text
136
134
 
137
135
  @current_text << string
@@ -139,6 +137,11 @@ module Uniword
139
137
 
140
138
  private
141
139
 
140
+ # Strip any namespace prefix from a SAX element name.
141
+ def local_name(name)
142
+ name.split(":").last
143
+ end
144
+
142
145
  # Check if we've hit paragraph limit
143
146
  def paragraph_limit_reached?
144
147
  @paragraph_limit && @paragraph_count >= @paragraph_limit
@@ -153,7 +156,7 @@ module Uniword
153
156
  def start_paragraph
154
157
  return if paragraph_limit_reached?
155
158
 
156
- @current_paragraph = Paragraph.new
159
+ @current_paragraph = Wordprocessingml::Paragraph.new
157
160
  @paragraph_count += 1
158
161
  end
159
162
 
@@ -176,7 +179,7 @@ module Uniword
176
179
  def start_run
177
180
  return unless @current_paragraph
178
181
 
179
- @current_run = Run.new
182
+ @current_run = Wordprocessingml::Run.new
180
183
  end
181
184
 
182
185
  # End parsing a run
@@ -188,7 +191,7 @@ module Uniword
188
191
  end
189
192
 
190
193
  # Start parsing text
191
- def start_text(_attrs)
194
+ def start_text
192
195
  return unless @current_run
193
196
 
194
197
  @current_text = +""
@@ -198,8 +201,7 @@ module Uniword
198
201
  def end_text
199
202
  return unless @current_text && @current_run
200
203
 
201
- text_element = TextElement.new(content: @current_text)
202
- @current_run.text_element = text_element
204
+ @current_run.text = [Wordprocessingml::Text.new(content: @current_text)]
203
205
  @current_text = nil
204
206
  end
205
207
 
@@ -207,7 +209,7 @@ module Uniword
207
209
  def start_table
208
210
  return if table_limit_reached?
209
211
 
210
- @current_table = Table.new
212
+ @current_table = Wordprocessingml::Table.new
211
213
  @table_count += 1
212
214
  end
213
215
 
@@ -223,7 +225,7 @@ module Uniword
223
225
  def start_table_row
224
226
  return unless @current_table
225
227
 
226
- @current_row = TableRow.new
228
+ @current_row = Wordprocessingml::TableRow.new
227
229
  end
228
230
 
229
231
  # End parsing a table row
@@ -238,7 +240,7 @@ module Uniword
238
240
  def start_table_cell
239
241
  return unless @current_row
240
242
 
241
- @current_cell = TableCell.new
243
+ @current_cell = Wordprocessingml::TableCell.new
242
244
  end
243
245
 
244
246
  # End parsing a table cell
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
4
-
5
3
  module Uniword
6
4
  module Themes
7
5
  # Parses theme XML files into Theme models
@@ -26,7 +24,7 @@ module Uniword
26
24
  # @return [Theme] Parsed theme
27
25
  # @raise [ArgumentError] if XML is invalid or missing theme element
28
26
  def parse(xml)
29
- doc = Nokogiri::XML(xml)
27
+ doc = Moxml.parse(xml)
30
28
  theme_node = doc.at_xpath("//a:theme", THEME_NS)
31
29
 
32
30
  unless theme_node
@@ -63,7 +61,7 @@ module Uniword
63
61
 
64
62
  # Parse color scheme from XML
65
63
  #
66
- # @param node [Nokogiri::XML::Element] Color scheme node
64
+ # @param node [Moxml::Element] Color scheme node
67
65
  # @return [ColorScheme] Parsed color scheme
68
66
  def parse_color_scheme(node)
69
67
  scheme = ColorScheme.new
@@ -140,7 +138,7 @@ module Uniword
140
138
  # - schemeClr: Reference to another theme color
141
139
  # - prstClr: Preset color name
142
140
  #
143
- # @param node [Nokogiri::XML::Element] Color node
141
+ # @param node [Moxml::Element] Color node
144
142
  # @return [String, nil] RGB hex value (6 characters, no #)
145
143
  def extract_color_value(node)
146
144
  # Try sRGB color first (direct RGB value)
@@ -182,7 +180,7 @@ module Uniword
182
180
 
183
181
  # Parse font scheme from XML
184
182
  #
185
- # @param node [Nokogiri::XML::Element] Font scheme node
183
+ # @param node [Moxml::Element] Font scheme node
186
184
  # @return [FontScheme] Parsed font scheme
187
185
  def parse_font_scheme(node)
188
186
  scheme = FontScheme.new
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Uniword
4
4
  module Transformation
5
- # Builds OOXML element objects from parsed HTML (Nokogiri nodes).
5
+ # Builds OOXML element objects from parsed HTML (Moxml nodes).
6
6
  #
7
7
  # Pure functions — no state, no side effects.
8
8
  # Used by HtmlToOoxmlConverter to construct OOXML paragraphs, tables, runs.
@@ -11,7 +11,7 @@ module Uniword
11
11
  class HtmlElementBuilder
12
12
  # Build an OOXML Paragraph from an HTML element.
13
13
  #
14
- # @param element [Nokogiri::XML::Element] HTML element
14
+ # @param element [Moxml::Element] HTML element
15
15
  # @return [Uniword::Wordprocessingml::Paragraph, nil]
16
16
  def self.build_paragraph(element)
17
17
  paragraph = Uniword::Wordprocessingml::Paragraph.new
@@ -32,11 +32,11 @@ module Uniword
32
32
  # cell insertion. The grid layout is computed row-by-row to correctly
33
33
  # place vMerge continuation cells where HTML rowspan omits cells.
34
34
  #
35
- # @param html_table [Nokogiri::XML::Element] HTML table element
35
+ # @param html_table [Moxml::Element] HTML table element
36
36
  # @return [Uniword::Wordprocessingml::Table, nil]
37
37
  def self.build_table(html_table)
38
38
  table = Uniword::Wordprocessingml::Table.new
39
- html_rows = html_table.css("tr")
39
+ html_rows = html_table.xpath(".//tr")
40
40
  return nil if html_rows.empty?
41
41
 
42
42
  # Track columns occupied by rowspan: col_idx → remaining continuation rows
@@ -47,7 +47,7 @@ module Uniword
47
47
  row = Uniword::Wordprocessingml::TableRow.new
48
48
  col_idx = 0
49
49
 
50
- html_row.css("td, th").each do |html_cell|
50
+ html_row.xpath(".//td | .//th").each do |html_cell|
51
51
  # Insert vMerge continuation cells for columns occupied by rowspan
52
52
  while occupied.key?(col_idx)
53
53
  row.cells << create_vmerge_continuation_cell
@@ -60,13 +60,13 @@ module Uniword
60
60
  row.cells << cell
61
61
 
62
62
  # Track rowspan for continuation cell insertion in subsequent rows
63
- rowspan = html_cell.attr("rowspan")&.to_i
63
+ rowspan = html_cell.attribute("rowspan")&.value&.to_i
64
64
  if rowspan && rowspan > 1
65
- colspan = html_cell.attr("colspan")&.to_i || 1
65
+ colspan = html_cell.attribute("colspan")&.value&.to_i || 1
66
66
  colspan.times { |c| occupied[col_idx + c] = rowspan - 1 }
67
67
  end
68
68
 
69
- col_idx += html_cell.attr("colspan")&.to_i || 1
69
+ col_idx += html_cell.attribute("colspan")&.value&.to_i || 1
70
70
  end
71
71
 
72
72
  # Handle remaining occupied columns at end of row
@@ -88,13 +88,13 @@ module Uniword
88
88
 
89
89
  # Build an OOXML TableRow from an HTML tr element.
90
90
  #
91
- # @param html_row [Nokogiri::XML::Element] HTML tr element
91
+ # @param html_row [Moxml::Element] HTML tr element
92
92
  # @return [Uniword::Wordprocessingml::TableRow, nil]
93
93
  def self.build_row(html_row)
94
94
  row = Uniword::Wordprocessingml::TableRow.new
95
95
  cells = []
96
96
 
97
- html_row.css("td, th").each do |html_cell|
97
+ html_row.xpath(".//td | .//th").each do |html_cell|
98
98
  cell = build_cell(html_cell)
99
99
  cells << cell if cell
100
100
  end
@@ -109,7 +109,7 @@ module Uniword
109
109
  #
110
110
  # Handles colspan → gridSpan, rowspan → vMerge restart, <th> → header.
111
111
  #
112
- # @param html_cell [Nokogiri::XML::Element] HTML td/th element
112
+ # @param html_cell [Moxml::Element] HTML td/th element
113
113
  # @return [Uniword::Wordprocessingml::TableCell, nil]
114
114
  def self.build_cell(html_cell)
115
115
  cell = Uniword::Wordprocessingml::TableCell.new
@@ -118,8 +118,8 @@ module Uniword
118
118
  cell.header = html_cell.name.downcase == "th"
119
119
 
120
120
  # Handle colspan → gridSpan, rowspan → vMerge restart
121
- colspan = html_cell.attr("colspan")
122
- rowspan = html_cell.attr("rowspan")
121
+ colspan = html_cell.attribute("colspan")&.value
122
+ rowspan = html_cell.attribute("rowspan")&.value
123
123
 
124
124
  if (colspan && colspan.to_i > 1) || (rowspan && rowspan.to_i > 1)
125
125
  cell.properties ||= Uniword::Wordprocessingml::TableCellProperties.new
@@ -134,7 +134,9 @@ module Uniword
134
134
  end
135
135
 
136
136
  # Convert cell content to paragraphs
137
- html_cell.css("p, div, h1, h2, h3, h4, h5, h6").each do |para_element|
137
+ block_tags = ".//p | .//div | .//h1 | .//h2 | .//h3 | .//h4 | " \
138
+ ".//h5 | .//h6"
139
+ html_cell.xpath(block_tags).each do |para_element|
138
140
  paragraph = build_paragraph(para_element)
139
141
  cell.paragraphs << paragraph if paragraph
140
142
  end
@@ -165,7 +167,7 @@ module Uniword
165
167
 
166
168
  # Create an OOXML Run from an HTML element with inline formatting.
167
169
  #
168
- # @param element [Nokogiri::XML::Element] HTML element
170
+ # @param element [Moxml::Element] HTML element
169
171
  # @return [Uniword::Wordprocessingml::Run, nil]
170
172
  def self.create_run_from_element(element)
171
173
  text = element.text.strip
@@ -182,7 +184,7 @@ module Uniword
182
184
 
183
185
  # Create an OOXML SDT from an HTML w:sdt element.
184
186
  #
185
- # @param element [Nokogiri::XML::Element] HTML w:sdt element
187
+ # @param element [Moxml::Element] HTML w:sdt element
186
188
  # @return [Uniword::Wordprocessingml::StructuredDocumentTag, nil]
187
189
  def self.create_sdt_from_element(element)
188
190
  text = element.text.strip
@@ -213,7 +215,7 @@ module Uniword
213
215
 
214
216
  # Apply CSS class → OOXML style mapping.
215
217
  def self.apply_css_style(paragraph, element)
216
- css_class = element.attr("class")
218
+ css_class = element.attribute("class")&.value
217
219
  return unless css_class && !css_class.empty?
218
220
 
219
221
  mapped_style = HtmlFormattingMapper.map_css_class_to_style(css_class)
@@ -230,13 +232,12 @@ module Uniword
230
232
  # → FootnoteReference run, <span class="MsoEndnoteReference"> → EndnoteReference run.
231
233
  def self.build_children(paragraph, element)
232
234
  element.children.each do |child|
233
- case child.type
234
- when Nokogiri::XML::Node::TEXT_NODE
235
+ if child.is_a?(Moxml::Text)
235
236
  text = child.text.strip
236
237
  next if text.empty?
237
238
 
238
239
  paragraph.runs << create_run(text)
239
- when Nokogiri::XML::Node::ELEMENT_NODE
240
+ elsif child.is_a?(Moxml::Element)
240
241
  case child.name.downcase
241
242
  when "w:sdt", "sdt"
242
243
  sdt = create_sdt_from_element(child)
@@ -273,44 +274,53 @@ module Uniword
273
274
 
274
275
  # Parse SDT attributes from HTML element.
275
276
  def self.parse_sdt_attributes(element)
276
- attrs = element.attributes
277
- return nil if attrs.empty?
277
+ return nil if element.attributes.empty?
278
278
 
279
279
  sdt_props =
280
280
  Uniword::Wordprocessingml::StructuredDocumentTagProperties.new
281
281
 
282
- if attrs["showingplchdr"] || attrs["ShowingPlcHdr"]
282
+ if attr_value(element, "showingplchdr") ||
283
+ attr_value(element, "ShowingPlcHdr")
283
284
  sdt_props.showing_placeholder_header =
284
285
  Uniword::Wordprocessingml::StructuredDocumentTag::ShowingPlaceholderHeader.new
285
286
  end
286
287
 
287
- if attrs["temporary"] || attrs["Temporary"]
288
+ if attr_value(element, "temporary") ||
289
+ attr_value(element, "Temporary")
288
290
  sdt_props.temporary =
289
291
  Uniword::Wordprocessingml::StructuredDocumentTag::Temporary.new
290
292
  end
291
293
 
292
- doc_part = attrs["docpart"] || attrs["DocPart"]
294
+ doc_part = attr_value(element, "docpart") ||
295
+ attr_value(element, "DocPart")
293
296
  if doc_part
294
297
  placeholder =
295
298
  Uniword::Wordprocessingml::StructuredDocumentTag::Placeholder.new
296
299
  doc_part_ref =
297
- Uniword::Wordprocessingml::StructuredDocumentTag::DocPartReference.new(value: doc_part.value)
300
+ Uniword::Wordprocessingml::StructuredDocumentTag::DocPartReference.new(
301
+ value: doc_part,
302
+ )
298
303
  placeholder.doc_part = doc_part_ref
299
304
  sdt_props.placeholder = placeholder
300
305
  end
301
306
 
302
- if attrs["text"] || attrs["Text"]
307
+ if attr_value(element, "text") || attr_value(element, "Text")
303
308
  sdt_props.text =
304
- Uniword::Wordprocessingml::StructuredDocumentTag::Text.new(value: "whole")
309
+ Uniword::Wordprocessingml::StructuredDocumentTag::Text.new(
310
+ value: "whole",
311
+ )
305
312
  end
306
313
 
307
- id_attr = attrs["id"] || attrs["ID"]
314
+ id_attr = attr_value(element, "id") || attr_value(element, "ID")
308
315
  if id_attr
309
316
  sdt_props.id =
310
- Uniword::Wordprocessingml::StructuredDocumentTag::Id.new(value: id_attr.value.to_i)
317
+ Uniword::Wordprocessingml::StructuredDocumentTag::Id.new(
318
+ value: id_attr.to_i,
319
+ )
311
320
  end
312
321
 
313
- if attrs["bibliography"] || attrs["Bibliography"]
322
+ if attr_value(element, "bibliography") ||
323
+ attr_value(element, "Bibliography")
314
324
  sdt_props.bibliography =
315
325
  Uniword::Wordprocessingml::StructuredDocumentTag::Bibliography.new
316
326
  end
@@ -318,15 +328,20 @@ module Uniword
318
328
  sdt_props
319
329
  end
320
330
 
331
+ # Read an attribute value from a Moxml element by name.
332
+ def self.attr_value(element, name)
333
+ element.attribute(name)&.value
334
+ end
335
+
321
336
  # Create a Run with Break from an HTML <br> element.
322
337
  #
323
- # @param element [Nokogiri::XML::Element] <br> element
338
+ # @param element [Moxml::Element] <br> element
324
339
  # @return [Uniword::Wordprocessingml::Run]
325
340
  def self.create_break_run(element)
326
341
  run = Uniword::Wordprocessingml::Run.new
327
342
  brk = Uniword::Wordprocessingml::Break.new
328
- style = element.attr("style").to_s
329
- clear = element.attr("clear").to_s
343
+ style = element.attribute("style")&.value.to_s
344
+ clear = element.attribute("clear")&.value.to_s
330
345
  if style.include?("page-break-before") || style.include?("page-break-after") || clear == "all"
331
346
  brk.type = "page"
332
347
  end
@@ -336,27 +351,27 @@ module Uniword
336
351
 
337
352
  # Check if element is a footnote reference span.
338
353
  #
339
- # @param element [Nokogiri::XML::Element]
354
+ # @param element [Moxml::Element]
340
355
  # @return [Boolean]
341
356
  def self.footnote_reference_span?(element)
342
357
  return false unless element.name.downcase == "span"
343
358
 
344
- element.attr("class").to_s.split.include?("MsoFootnoteReference")
359
+ element.attribute("class")&.value.to_s.split.include?("MsoFootnoteReference")
345
360
  end
346
361
 
347
362
  # Check if element is an endnote reference span.
348
363
  #
349
- # @param element [Nokogiri::XML::Element]
364
+ # @param element [Moxml::Element]
350
365
  # @return [Boolean]
351
366
  def self.endnote_reference_span?(element)
352
367
  return false unless element.name.downcase == "span"
353
368
 
354
- element.attr("class").to_s.split.include?("MsoEndnoteReference")
369
+ element.attribute("class")&.value.to_s.split.include?("MsoEndnoteReference")
355
370
  end
356
371
 
357
372
  # Create a Run with FootnoteReference from an HTML footnote reference span.
358
373
  #
359
- # @param element [Nokogiri::XML::Element] Span element with MsoFootnoteReference class
374
+ # @param element [Moxml::Element] Span with MsoFootnoteReference class
360
375
  # @return [Uniword::Wordprocessingml::Run]
361
376
  def self.create_footnote_reference_run(element)
362
377
  run = Uniword::Wordprocessingml::Run.new
@@ -368,7 +383,7 @@ module Uniword
368
383
 
369
384
  # Create a Run with EndnoteReference from an HTML endnote reference span.
370
385
  #
371
- # @param element [Nokogiri::XML::Element] Span element with MsoEndnoteReference class
386
+ # @param element [Moxml::Element] Span with MsoEndnoteReference class
372
387
  # @return [Uniword::Wordprocessingml::Run]
373
388
  def self.create_endnote_reference_run(element)
374
389
  run = Uniword::Wordprocessingml::Run.new
@@ -382,13 +397,13 @@ module Uniword
382
397
  #
383
398
  # Tries: nested <a href="#_ftnN">, then text content digits.
384
399
  #
385
- # @param element [Nokogiri::XML::Element]
400
+ # @param element [Moxml::Element]
386
401
  # @return [String, nil]
387
402
  def self.extract_note_id(element)
388
403
  # Try nested <a href="#_ftnN"> or <a href="#_ednN">
389
- anchor = element.at_css("a[href^='#']")
404
+ anchor = element.at_xpath('.//a[starts-with(@href, "#")]')
390
405
  if anchor
391
- href = anchor.attr("href").sub(/^#/, "")
406
+ href = anchor.attribute("href")&.value&.sub(/^#/, "")
392
407
  if href =~ /(\d+)\s*$/
393
408
  return Regexp.last_match(1)
394
409
  end
@@ -405,23 +420,22 @@ module Uniword
405
420
 
406
421
  # Create a Hyperlink from an HTML <a href> element.
407
422
  #
408
- # @param element [Nokogiri::XML::Element] <a> element
423
+ # @param element [Moxml::Element] <a> element
409
424
  # @return [Uniword::Wordprocessingml::Hyperlink, nil]
410
425
  def self.create_hyperlink(element)
411
- href = element.attr("href")
426
+ href = element.attribute("href")&.value
412
427
  return nil unless href
413
428
 
414
429
  hyperlink = Uniword::Wordprocessingml::Hyperlink.new
415
430
  hyperlink.target = href
416
431
 
417
432
  element.children.each do |child|
418
- case child.type
419
- when Nokogiri::XML::Node::TEXT_NODE
433
+ if child.is_a?(Moxml::Text)
420
434
  text = child.text.strip
421
435
  next if text.empty?
422
436
 
423
437
  hyperlink.runs << create_run(text)
424
- when Nokogiri::XML::Node::ELEMENT_NODE
438
+ elsif child.is_a?(Moxml::Element)
425
439
  run = create_run_from_element(child)
426
440
  hyperlink.runs << run if run
427
441
  end
@@ -30,7 +30,7 @@ module Uniword
30
30
  #
31
31
  # Handles nested formatting like <u><em><strong>text</strong></em></u>.
32
32
  #
33
- # @param element [Nokogiri::XML::Element] HTML element
33
+ # @param element [Moxml::Element] HTML element
34
34
  # @return [Uniword::Wordprocessingml::RunProperties]
35
35
  def self.collect_formatting(element)
36
36
  props = Uniword::Wordprocessingml::RunProperties.new
@@ -118,10 +118,10 @@ module Uniword
118
118
 
119
119
  # Apply formatting from span element's style attribute.
120
120
  #
121
- # @param element [Nokogiri::XML::Element] span element
121
+ # @param element [Moxml::Element] span element
122
122
  # @param props [Uniword::Wordprocessingml::RunProperties]
123
123
  def self.apply_span_styles(element, props)
124
- style = element.attr("style") || ""
124
+ style = element.attribute("style")&.value || ""
125
125
 
126
126
  if style =~ /color:\s*#?([0-9a-fA-F]{6})/i
127
127
  props.color =
@@ -18,15 +18,15 @@ module Uniword
18
18
  return [] if html_content.nil? || html_content.empty?
19
19
 
20
20
  body = HtmlFormattingMapper.extract_body(html_content)
21
- doc = Nokogiri::HTML(body)
21
+ doc = Moxml.new.parse_html(body)
22
22
  paragraphs = []
23
23
 
24
- doc.css("p, h1, h2, h3, h4, h5, h6, li, div, tr").each do |element|
25
- next if element.ancestors("td, th").any?
24
+ doc.xpath(".//p | .//h1 | .//h2 | .//h3 | .//h4 | .//h5 | .//h6 | .//li | .//div | .//tr").each do |element|
25
+ next if element.ancestors("td").any? || element.ancestors("th").any?
26
26
  next if %w[tr td].include?(element.name)
27
27
 
28
28
  if %w[div li].include?(element.name) &&
29
- element.css("p, h1, h2, h3, h4, h5, h6, li, div, tr").any?
29
+ element.xpath(".//p | .//h1 | .//h2 | .//h3 | .//h4 | .//h5 | .//h6 | .//li | .//div | .//tr").any?
30
30
  next
31
31
  end
32
32
 
@@ -45,10 +45,10 @@ module Uniword
45
45
  return [] if html_content.nil? || html_content.empty?
46
46
 
47
47
  body = HtmlFormattingMapper.extract_body(html_content)
48
- doc = Nokogiri::HTML(body)
48
+ doc = Moxml.new.parse_html(body)
49
49
  tables = []
50
50
 
51
- doc.css("table").each do |html_table|
51
+ doc.xpath(".//table").each do |html_table|
52
52
  table = HtmlElementBuilder.build_table(html_table)
53
53
  tables << table if table
54
54
  end
@@ -58,7 +58,7 @@ module Uniword
58
58
 
59
59
  # Convert a single HTML table to OOXML table.
60
60
  #
61
- # @param html_table [Nokogiri::XML::Element] HTML table element
61
+ # @param html_table [Moxml::Element] HTML table element
62
62
  # @return [Uniword::Wordprocessingml::Table, nil]
63
63
  def self.html_table_to_table(html_table)
64
64
  HtmlElementBuilder.build_table(html_table)
@@ -66,7 +66,7 @@ module Uniword
66
66
 
67
67
  # Convert a single HTML row to OOXML table row.
68
68
  #
69
- # @param html_row [Nokogiri::XML::Element] HTML tr element
69
+ # @param html_row [Moxml::Element] HTML tr element
70
70
  # @return [Uniword::Wordprocessingml::TableRow, nil]
71
71
  def self.html_row_to_row(html_row)
72
72
  HtmlElementBuilder.build_row(html_row)
@@ -74,7 +74,7 @@ module Uniword
74
74
 
75
75
  # Convert a single HTML cell to OOXML table cell.
76
76
  #
77
- # @param html_cell [Nokogiri::XML::Element] HTML td/th element
77
+ # @param html_cell [Moxml::Element] HTML td/th element
78
78
  # @return [Uniword::Wordprocessingml::TableCell, nil]
79
79
  def self.html_cell_to_cell(html_cell)
80
80
  HtmlElementBuilder.build_cell(html_cell)
@@ -82,7 +82,7 @@ module Uniword
82
82
 
83
83
  # Convert a single HTML element to OOXML paragraph.
84
84
  #
85
- # @param element [Nokogiri::XML::Element] HTML element
85
+ # @param element [Moxml::Element] HTML element
86
86
  # @return [Uniword::Wordprocessingml::Paragraph, nil]
87
87
  def self.html_element_to_paragraph(element)
88
88
  HtmlElementBuilder.build_paragraph(element)
@@ -98,7 +98,7 @@ module Uniword
98
98
 
99
99
  # Create a run from HTML element with inline formatting.
100
100
  #
101
- # @param element [Nokogiri::XML::Element] HTML element
101
+ # @param element [Moxml::Element] HTML element
102
102
  # @return [Uniword::Wordprocessingml::Run, nil]
103
103
  def self.create_run_from_element(element)
104
104
  HtmlElementBuilder.create_run_from_element(element)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
4
3
 
5
4
  module Uniword
6
5
  module Transformation
@@ -625,7 +624,7 @@ module Uniword
625
624
  return "" unless content
626
625
 
627
626
  xml = content.to_xml
628
- doc = Nokogiri::HTML(xml)
627
+ doc = Moxml.new.parse_html(xml)
629
628
  doc.text.gsub(/\s+/, " ").strip
630
629
  end
631
630
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zip"
4
- require "nokogiri"
5
4
 
6
5
  module Uniword
7
6
  module Validation
@@ -107,7 +106,7 @@ module Uniword
107
106
  ct_entry = zip.find_entry("[Content_Types].xml")
108
107
  return unless ct_entry
109
108
 
110
- ct_doc = Nokogiri::XML(ct_entry.get_input_stream.read, &:strict)
109
+ ct_doc = Moxml.parse(ct_entry.get_input_stream.read)
111
110
 
112
111
  # Get declared extensions
113
112
  declared_exts = ct_doc.xpath("//xmlns:Default", "xmlns" => CT_NS)
@@ -137,7 +136,7 @@ module Uniword
137
136
  suggestion: "Add a Default or Override entry in [Content_Types].xml.",
138
137
  )
139
138
  end
140
- rescue Nokogiri::XML::SyntaxError => e
139
+ rescue Moxml::ParseError => e
141
140
  issues << Report::ValidationIssue.new(
142
141
  severity: "error",
143
142
  code: "OPC-008",
@@ -155,7 +154,7 @@ module Uniword
155
154
  end
156
155
 
157
156
  def check_relationship_file(zip, entry, issues)
158
- doc = Nokogiri::XML(entry.get_input_stream.read)
157
+ doc = Moxml.parse(entry.get_input_stream.read)
159
158
 
160
159
  base_dir = compute_base_dir(entry.name)
161
160
 
@@ -181,7 +180,7 @@ module Uniword
181
180
  "from the package. Remove the relationship or add the part.",
182
181
  )
183
182
  end
184
- rescue Nokogiri::XML::SyntaxError => e
183
+ rescue Moxml::ParseError => e
185
184
  issues << Report::ValidationIssue.new(
186
185
  severity: "error",
187
186
  code: "OPC-008",
@@ -195,8 +194,8 @@ module Uniword
195
194
 
196
195
  xml_entries.each do |entry|
197
196
  content = entry.get_input_stream.read
198
- Nokogiri::XML(content, &:strict)
199
- rescue Nokogiri::XML::SyntaxError => e
197
+ Moxml.parse(content)
198
+ rescue Moxml::ParseError => e
200
199
  issues << Report::ValidationIssue.new(
201
200
  severity: "error",
202
201
  code: "OPC-008",