uniword 1.0.4 → 1.0.6

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/uniword/accessibility/rules/descriptive_headings_rule.rb +7 -9
  3. data/lib/uniword/accessibility/rules/document_title_rule.rb +2 -2
  4. data/lib/uniword/accessibility/rules/heading_structure_rule.rb +4 -3
  5. data/lib/uniword/accessibility/rules/language_specification_rule.rb +2 -2
  6. data/lib/uniword/accessibility/rules/table_headers_rule.rb +1 -1
  7. data/lib/uniword/assembly/cross_reference_resolver.rb +6 -59
  8. data/lib/uniword/assembly/toc.rb +2 -2
  9. data/lib/uniword/assembly/variable_substitutor.rb +0 -24
  10. data/lib/uniword/batch/stages/compress_images_stage.rb +2 -2
  11. data/lib/uniword/batch/stages/normalize_styles_stage.rb +12 -12
  12. data/lib/uniword/batch/stages/update_metadata_stage.rb +26 -11
  13. data/lib/uniword/batch/stages/validate_links_stage.rb +1 -1
  14. data/lib/uniword/builder/bibliography_builder.rb +1 -1
  15. data/lib/uniword/builder/chart_builder.rb +2 -2
  16. data/lib/uniword/builder/has_borders.rb +1 -1
  17. data/lib/uniword/builder/image_builder.rb +1 -1
  18. data/lib/uniword/cli/main.rb +4 -4
  19. data/lib/uniword/diff/document_differ.rb +11 -7
  20. data/lib/uniword/document_factory.rb +3 -3
  21. data/lib/uniword/docx/document_statistics.rb +3 -3
  22. data/lib/uniword/docx/package_defaults.rb +2 -2
  23. data/lib/uniword/docx/reconciler.rb +10 -28
  24. data/lib/uniword/element_registry.rb +1 -1
  25. data/lib/uniword/endnote.rb +3 -1
  26. data/lib/uniword/footnote.rb +3 -1
  27. data/lib/uniword/format_converter.rb +7 -7
  28. data/lib/uniword/format_detector.rb +1 -1
  29. data/lib/uniword/generation/document_generator.rb +2 -2
  30. data/lib/uniword/infrastructure/zip_extractor.rb +1 -1
  31. data/lib/uniword/math_equation.rb +3 -1
  32. data/lib/uniword/mhtml/math_converter.rb +1 -1
  33. data/lib/uniword/mhtml/word_css.rb +14 -14
  34. data/lib/uniword/ooxml/schema/element_serializer.rb +14 -14
  35. data/lib/uniword/quality/document_checker.rb +2 -2
  36. data/lib/uniword/quality/rules/link_validation_rule.rb +4 -4
  37. data/lib/uniword/quality/rules/style_consistency_rule.rb +1 -1
  38. data/lib/uniword/quality/rules/table_header_rule.rb +1 -1
  39. data/lib/uniword/resource/color_transformer.rb +1 -1
  40. data/lib/uniword/resource/resource_resolver.rb +1 -1
  41. data/lib/uniword/resource/theme_processor.rb +1 -1
  42. data/lib/uniword/resource/theme_transition.rb +1 -1
  43. data/lib/uniword/review/interactive_review.rb +1 -1
  44. data/lib/uniword/spellcheck/spell_checker.rb +6 -6
  45. data/lib/uniword/styleset.rb +1 -1
  46. data/lib/uniword/template/helpers/conditional_helper.rb +3 -4
  47. data/lib/uniword/template/helpers/filter_helper.rb +1 -1
  48. data/lib/uniword/template/helpers/loop_helper.rb +1 -1
  49. data/lib/uniword/template/helpers/variable_helper.rb +1 -1
  50. data/lib/uniword/template/template_parser.rb +9 -7
  51. data/lib/uniword/template/template_renderer.rb +3 -3
  52. data/lib/uniword/template/variable_resolver.rb +5 -6
  53. data/lib/uniword/themes/theme_transformation.rb +1 -1
  54. data/lib/uniword/toc/toc_generator.rb +1 -1
  55. data/lib/uniword/transformation/mhtml_element_renderer.rb +5 -7
  56. data/lib/uniword/transformation/mhtml_metadata_builder.rb +17 -18
  57. data/lib/uniword/transformation/ooxml_to_html_converter.rb +1 -1
  58. data/lib/uniword/transformation/transformer.rb +6 -6
  59. data/lib/uniword/validation/checkers/external_link_checker.rb +1 -1
  60. data/lib/uniword/validation/checkers/file_reference_checker.rb +5 -5
  61. data/lib/uniword/validation/checkers/footnote_reference_checker.rb +12 -12
  62. data/lib/uniword/validation/checkers/internal_link_checker.rb +7 -9
  63. data/lib/uniword/validation/link_validator.rb +17 -23
  64. data/lib/uniword/validation/validation_report.rb +5 -3
  65. data/lib/uniword/validation/validation_result.rb +4 -4
  66. data/lib/uniword/validators/element_validator.rb +1 -7
  67. data/lib/uniword/version.rb +1 -1
  68. data/lib/uniword/visitor/text_extractor.rb +3 -3
  69. data/lib/uniword/warnings/warning_collector.rb +1 -1
  70. data/lib/uniword/watermark/manager.rb +1 -1
  71. data/lib/uniword/wordprocessingml/numbering_configuration.rb +1 -1
  72. data/lib/uniword/wordprocessingml/run.rb +4 -4
  73. data/lib/uniword/wordprocessingml/styles/style_definition.rb +1 -1
  74. metadata +2 -2
@@ -390,12 +390,7 @@ module Uniword
390
390
  record_fix("R2", "Generated w15:docId in GUID format")
391
391
  end
392
392
 
393
- unless settings.mc_ignorable
394
- settings.mc_ignorable = Ooxml::Types::McIgnorable.new(
395
- "w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du",
396
- )
397
- record_fix("R1", "Added mc:Ignorable to settings")
398
- end
393
+ settings.mc_ignorable = nil
399
394
  end
400
395
 
401
396
  def reconcile_font_table
@@ -436,9 +431,7 @@ module Uniword
436
431
  font_table.fonts << font
437
432
  end
438
433
 
439
- font_table.mc_ignorable ||= Ooxml::Types::McIgnorable.new(
440
- "w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du",
441
- )
434
+ font_table.mc_ignorable = nil
442
435
  record_fix("R13",
443
436
  "Populated font table with profile fonts and signatures")
444
437
  end
@@ -457,9 +450,7 @@ module Uniword
457
450
 
458
451
  ensure_default_styles(styles)
459
452
 
460
- styles.mc_ignorable ||= Ooxml::Types::McIgnorable.new(
461
- "w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du",
462
- )
453
+ styles.mc_ignorable = nil
463
454
  record_fix("R10",
464
455
  "Ensured styles have docDefaults, latentStyles, and default styles")
465
456
  end
@@ -468,18 +459,13 @@ module Uniword
468
459
  return unless profile
469
460
  return unless package.numbering
470
461
 
471
- unless package.numbering.mc_ignorable
472
- package.numbering.mc_ignorable = Ooxml::Types::McIgnorable.new(
473
- "w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14",
474
- )
475
- record_fix("R1", "Added mc:Ignorable to numbering")
476
- end
462
+ package.numbering.mc_ignorable = nil
477
463
 
478
464
  # Validate instance → definition references
479
465
  package.numbering.instances.each do |inst|
480
466
  next unless inst.abstract_num_id
481
467
 
482
- abs_id = inst.abstract_num_id.respond_to?(:val) ? inst.abstract_num_id.val : inst.abstract_num_id
468
+ abs_id = inst.abstract_num_id.is_a?(Uniword::Wordprocessingml::AbstractNumId) ? inst.abstract_num_id.val : inst.abstract_num_id
483
469
  defn = package.numbering.definitions.find do |d|
484
470
  d.abstract_num_id == abs_id
485
471
  end
@@ -499,10 +485,8 @@ module Uniword
499
485
  package.web_settings
500
486
  end
501
487
 
502
- ws.mc_ignorable ||= Ooxml::Types::McIgnorable.new(
503
- "w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du",
504
- )
505
- record_fix("R1", "Added mc:Ignorable to webSettings")
488
+ ws.mc_ignorable = nil
489
+ record_fix("R1", "Cleared mc:Ignorable on webSettings")
506
490
  end
507
491
 
508
492
  def reconcile_app_properties
@@ -596,9 +580,7 @@ module Uniword
596
580
  return unless package.document&.body
597
581
 
598
582
  doc = package.document
599
- doc.mc_ignorable ||= Ooxml::Types::McIgnorable.new(
600
- "w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14",
601
- )
583
+ doc.mc_ignorable = Ooxml::Types::McIgnorable.new("w14")
602
584
 
603
585
  record_fix("R1", "Added mc:Ignorable to document body")
604
586
  record_fix("R12", "Assigned rsid and paraId to paragraphs")
@@ -985,8 +967,8 @@ module Uniword
985
967
  # 4. East Asian light font, 5. Major font (headings)
986
968
  names << fs&.minor_font if fs&.minor_font
987
969
 
988
- ea_font = loc.respond_to?(:east_asian_font) && loc.east_asian_font
989
- ea_light = loc.respond_to?(:east_asian_light_font) && loc.east_asian_light_font
970
+ ea_font = loc&.east_asian_font
971
+ ea_light = loc&.east_asian_light_font
990
972
 
991
973
  # Default East Asian fonts for zh-CN when locale profile omits them
992
974
  if loc.east_asia_lang == "zh-CN"
@@ -15,7 +15,7 @@ module Uniword
15
15
  # @param element_class [Class] The element class to register
16
16
  # @return [void]
17
17
  def register(element_class)
18
- return if element_class.respond_to?(:abstract?) && element_class.abstract?
18
+ return if element_class <= Uniword::Element && element_class.abstract?
19
19
 
20
20
  key = element_class_key(element_class)
21
21
  return if key.nil? # Skip anonymous classes
@@ -50,7 +50,9 @@ module Uniword
50
50
  #
51
51
  # @return [String] The combined text of all paragraphs
52
52
  def text
53
- @content.map { |p| p.respond_to?(:text) ? p.text : p.to_s }.join("\n")
53
+ @content.map do |p|
54
+ p.is_a?(Uniword::Wordprocessingml::Paragraph) ? p.text : p.to_s
55
+ end.join("\n")
54
56
  end
55
57
 
56
58
  # Convert to hash representation.
@@ -49,7 +49,9 @@ module Uniword
49
49
  #
50
50
  # @return [String] The combined text of all paragraphs
51
51
  def text
52
- @content.map { |p| p.respond_to?(:text) ? p.text : p.to_s }.join("\n")
52
+ @content.map do |p|
53
+ p.is_a?(Uniword::Wordprocessingml::Paragraph) ? p.text : p.to_s
54
+ end.join("\n")
53
55
  end
54
56
 
55
57
  # Convert to hash representation.
@@ -278,18 +278,18 @@ module Uniword
278
278
  # @return [Hash] Statistics hash
279
279
  def document_stats(document)
280
280
  # Handle OOXML document
281
- if document.respond_to?(:paragraphs)
281
+ if document.is_a?(Uniword::Wordprocessingml::DocumentRoot) || document.is_a?(Uniword::Wordprocessingml::Body)
282
282
  return {
283
283
  paragraphs: document.paragraphs.count,
284
- tables: document.respond_to?(:tables) ? document.tables.count : 0,
285
- images: document.respond_to?(:images) ? document.images.count : 0
284
+ tables: document.is_a?(Uniword::Wordprocessingml::DocumentRoot) ? document.tables.count : 0,
285
+ images: document.is_a?(Uniword::Wordprocessingml::DocumentRoot) ? document.images.count : 0
286
286
  }
287
287
  end
288
288
 
289
289
  # Handle MHTML document - estimate from HTML content
290
- html = if document.respond_to?(:raw_html) && document.raw_html
290
+ html = if document.is_a?(Mhtml::Document) && document.raw_html
291
291
  document.raw_html
292
- elsif document.respond_to?(:html_content) && document.html_content
292
+ elsif document.is_a?(Mhtml::Document) && document.html_content
293
293
  document.html_content
294
294
  end
295
295
  if html
@@ -339,8 +339,8 @@ module Uniword
339
339
  def log_conversion_start(source, source_format, target, target_format)
340
340
  return unless @logger
341
341
 
342
- source_name = source.respond_to?(:path) ? source.path : source.to_s
343
- target_name = target.respond_to?(:path) ? target.path : target.to_s
342
+ source_name = source.is_a?(IO) ? source.path : source.to_s
343
+ target_name = target.is_a?(IO) ? target.path : target.to_s
344
344
 
345
345
  @logger.info(
346
346
  "Converting #{source_name} (#{source_format}) → #{target_name} (#{target_format})"
@@ -65,7 +65,7 @@ module Uniword
65
65
  return if path.is_a?(IO) || path.is_a?(StringIO)
66
66
 
67
67
  # For strings, validate as file path
68
- if path.respond_to?(:empty?) && path.empty?
68
+ if path.is_a?(String) && path.empty?
69
69
  raise ArgumentError,
70
70
  "Path cannot be empty"
71
71
  end
@@ -67,7 +67,7 @@ module Uniword
67
67
  end
68
68
 
69
69
  def import_styles(builder, source_doc)
70
- return unless source_doc.respond_to?(:styles_configuration)
70
+ return unless source_doc.is_a?(Uniword::Wordprocessingml::DocumentRoot)
71
71
  return unless source_doc.styles_configuration
72
72
 
73
73
  source_doc.styles_configuration.styles.each do |style|
@@ -79,7 +79,7 @@ module Uniword
79
79
  end
80
80
 
81
81
  def import_theme(builder, source_doc)
82
- return unless source_doc.respond_to?(:theme)
82
+ return unless source_doc.is_a?(Uniword::Wordprocessingml::DocumentRoot)
83
83
  return unless source_doc.theme
84
84
 
85
85
  builder.model.theme = source_doc.theme
@@ -126,7 +126,7 @@ module Uniword
126
126
  return if path.is_a?(IO) || path.is_a?(StringIO)
127
127
 
128
128
  # For strings, validate as file path
129
- if path.respond_to?(:empty?) && path.empty?
129
+ if path.is_a?(String) && path.empty?
130
130
  raise ArgumentError,
131
131
  "Path cannot be empty"
132
132
  end
@@ -152,7 +152,9 @@ module Uniword
152
152
  # If present, it should be a Plurimath formula
153
153
  return true if formula.nil?
154
154
 
155
- formula.respond_to?(:to_latex)
155
+ return false unless defined?(Plurimath::Formula)
156
+
157
+ formula.is_a?(Plurimath::Formula)
156
158
  end
157
159
  end
158
160
  end
@@ -117,7 +117,7 @@ module Uniword
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.respond_to?(:name)
120
+ return false unless element.is_a?(Nokogiri::XML::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)
@@ -78,14 +78,14 @@ module Uniword
78
78
  # @param section_name [String] The section name
79
79
  # @return [String, nil] The CSS @page rule
80
80
  def self.build_page_rule(section, section_name)
81
- properties = section.respond_to?(:properties) ? section.properties : nil
81
+ properties = section&.properties
82
82
  return nil unless properties
83
83
 
84
84
  rules = []
85
85
 
86
86
  # Page size
87
- width = properties.respond_to?(:page_width) && properties.page_width
88
- height = properties.respond_to?(:page_height) && properties.page_height
87
+ width = properties.page_width
88
+ height = properties.page_height
89
89
  if width && height
90
90
  # Convert from twips to inches (1 inch = 1440 twips)
91
91
  w_in = CssNumberFormatter.twips_to_in(width, precision: 1)
@@ -97,14 +97,14 @@ module Uniword
97
97
  end
98
98
 
99
99
  # Margins
100
- rules << "margin-top: #{CssNumberFormatter.twips_to_in(properties.margin_top)}" if properties.respond_to?(:margin_top) && properties.margin_top
101
- rules << "margin-bottom: #{CssNumberFormatter.twips_to_in(properties.margin_bottom)}" if properties.respond_to?(:margin_bottom) && properties.margin_bottom
102
- rules << "margin-left: #{CssNumberFormatter.twips_to_in(properties.margin_left)}" if properties.respond_to?(:margin_left) && properties.margin_left
103
- rules << "margin-right: #{CssNumberFormatter.twips_to_in(properties.margin_right)}" if properties.respond_to?(:margin_right) && properties.margin_right
100
+ rules << "margin-top: #{CssNumberFormatter.twips_to_in(properties.margin_top)}" if properties.margin_top
101
+ rules << "margin-bottom: #{CssNumberFormatter.twips_to_in(properties.margin_bottom)}" if properties.margin_bottom
102
+ rules << "margin-left: #{CssNumberFormatter.twips_to_in(properties.margin_left)}" if properties.margin_left
103
+ rules << "margin-right: #{CssNumberFormatter.twips_to_in(properties.margin_right)}" if properties.margin_right
104
104
 
105
105
  # Header/Footer margins
106
- rules << "mso-header-margin: #{CssNumberFormatter.twips_to_in(properties.header_margin)}" if properties.respond_to?(:header_margin) && properties.header_margin
107
- rules << "mso-footer-margin: #{CssNumberFormatter.twips_to_in(properties.footer_margin)}" if properties.respond_to?(:footer_margin) && properties.footer_margin
106
+ rules << "mso-header-margin: #{CssNumberFormatter.twips_to_in(properties.header_margin)}" if properties.header_margin
107
+ rules << "mso-footer-margin: #{CssNumberFormatter.twips_to_in(properties.footer_margin)}" if properties.footer_margin
108
108
 
109
109
  "@page #{section_name} {\n #{rules.join(";\n ")};\n}"
110
110
  end
@@ -127,16 +127,16 @@ module Uniword
127
127
  properties = []
128
128
 
129
129
  # Font properties
130
- properties << "font-family: '#{style.font}'" if style.respond_to?(:font) && style.font
131
- if style.respond_to?(:font_size) && style.font_size
130
+ properties << "font-family: '#{style.font}'" if style.font
131
+ if style.font_size
132
132
  properties << "font-size: #{CssNumberFormatter.format(style.font_size, 'pt',
133
133
  precision: 1)}"
134
134
  end
135
- properties << "font-weight: bold" if style.respond_to?(:bold) && style.bold
136
- properties << "font-style: italic" if style.respond_to?(:italic) && style.italic
135
+ properties << "font-weight: bold" if style.bold
136
+ properties << "font-style: italic" if style.italic
137
137
 
138
138
  # Paragraph properties
139
- properties << "text-align: #{style.alignment}" if style.respond_to?(:alignment) && style.alignment
139
+ properties << "text-align: #{style.alignment}" if style.alignment
140
140
 
141
141
  return nil if properties.empty?
142
142
 
@@ -47,7 +47,7 @@ module Uniword
47
47
 
48
48
  # If element has its own to_xml method, use it directly
49
49
  # This ensures proper serialization with all attributes and content
50
- if element.respond_to?(:to_xml) && !options[:use_schema]
50
+ if element.is_a?(Lutaml::Model::Serializable) && !options[:use_schema]
51
51
  xml_str = element.to_xml(pretty: options[:pretty])
52
52
 
53
53
  # Remove XML declaration unless standalone
@@ -169,7 +169,7 @@ module Uniword
169
169
 
170
170
  # Skip if no children and optional
171
171
  next if children.nil? && child_def.optional?
172
- next if children.respond_to?(:empty?) && children.empty? && child_def.optional?
172
+ next if children.is_a?(Array) && children.empty? && child_def.optional?
173
173
 
174
174
  # Serialize child elements
175
175
  if child_def.multiple?
@@ -215,9 +215,9 @@ module Uniword
215
215
  namespace_uri)
216
216
  node.content = child
217
217
  node
218
- elsif child.respond_to?(:is_a?) && child.is_a?(Element)
218
+ elsif child.is_a?(Uniword::Element)
219
219
  # Check if this is a TextElement that needs special handling
220
- if child.instance_of?(::Uniword::TextElement) && child.respond_to?(:content)
220
+ if child.instance_of?(::Uniword::TextElement) && child.is_a?(Uniword::TextElement)
221
221
  # TextElement: serialize as element with text content
222
222
  node = Nokogiri::XML::Node.new(local_name, doc)
223
223
  node.namespace = node.add_namespace_definition(prefix,
@@ -276,7 +276,7 @@ module Uniword
276
276
  namespace_uri)
277
277
  node.content = child
278
278
  node
279
- elsif child.respond_to?(:content)
279
+ elsif child.is_a?(Uniword::TextElement)
280
280
  # TextElement or similar - get the content
281
281
  node = Nokogiri::XML::Node.new(local_name, doc)
282
282
  node.namespace = node.add_namespace_definition(prefix,
@@ -302,10 +302,10 @@ module Uniword
302
302
  property_name = attr_def.property_name
303
303
 
304
304
  # Try to get value using property name
305
- if element.respond_to?(property_name)
306
- element.send(property_name)
307
- elsif element.respond_to?(:attributes) && element.attributes.is_a?(Hash) && element.attributes.key?(property_name)
308
- element.attributes[property_name]
305
+ if element.is_a?(Lutaml::Model::Serializable) && element.class.attributes.key?(property_name)
306
+ element.public_send(property_name)
307
+ elsif element.is_a?(Hash) && element.key?(property_name)
308
+ element[property_name]
309
309
  end
310
310
  end
311
311
 
@@ -318,10 +318,10 @@ module Uniword
318
318
  property_name = child_def.property_name
319
319
 
320
320
  # Try to get children using property name
321
- if element.respond_to?(property_name)
322
- element.send(property_name)
323
- elsif element.respond_to?(:attributes) && element.attributes.key?(property_name)
324
- element.attributes[property_name]
321
+ if element.is_a?(Lutaml::Model::Serializable) && element.class.attributes.key?(property_name)
322
+ element.public_send(property_name)
323
+ elsif element.is_a?(Hash) && element.key?(property_name)
324
+ element[property_name]
325
325
  end
326
326
  end
327
327
 
@@ -330,7 +330,7 @@ module Uniword
330
330
  # @param element [Object] Object to validate
331
331
  # @raise [ArgumentError] if not a valid element
332
332
  def validate_element(element)
333
- return if element.respond_to?(:to_xml)
333
+ return if element.is_a?(Lutaml::Model::Serializable)
334
334
 
335
335
  raise ArgumentError,
336
336
  "Element must respond to #to_xml, got #{element.class}"
@@ -133,10 +133,10 @@ module Uniword
133
133
  # @param document [Object] The document to validate
134
134
  # @raise [ArgumentError] if document is invalid
135
135
  def validate_document!(document)
136
- return if document.respond_to?(:paragraphs) && document.respond_to?(:tables)
136
+ return if document.is_a?(Uniword::Wordprocessingml::DocumentRoot)
137
137
 
138
138
  raise ArgumentError,
139
- "Document must respond to :paragraphs and :tables methods"
139
+ "Document must be a Uniword::Wordprocessingml::DocumentRoot"
140
140
  end
141
141
  end
142
142
 
@@ -71,7 +71,7 @@ module Uniword
71
71
  # @param link [Hyperlink] The link to check
72
72
  # @return [Boolean] true if internal link
73
73
  def internal_link?(link)
74
- link.respond_to?(:anchor) && !link.anchor.nil? && !link.anchor.empty?
74
+ link.is_a?(Lutaml::Model::Serializable) && link.class.attributes.key?(:anchor) && !link.anchor.nil? && !link.anchor.empty?
75
75
  end
76
76
 
77
77
  # Check if link is external (URL)
@@ -83,10 +83,10 @@ module Uniword
83
83
  return false unless link
84
84
 
85
85
  # Check for id attribute (relationship-based external link)
86
- return true if link.respond_to?(:id) && !link.id.nil? && !link.id.empty? && !internal_link?(link)
86
+ return true if link.is_a?(Lutaml::Model::Serializable) && link.class.attributes.key?(:id) && !link.id.nil? && !link.id.empty? && !internal_link?(link)
87
87
 
88
88
  # Check for target that looks like a URL
89
- return !link.target.start_with?("#") if link.respond_to?(:target) && !link.target.nil? && !link.target.empty?
89
+ return !link.target.start_with?("#") if link.is_a?(Lutaml::Model::Serializable) && link.class.attributes.key?(:target) && !link.target.nil? && !link.target.empty?
90
90
 
91
91
  false
92
92
  end
@@ -114,7 +114,7 @@ module Uniword
114
114
  # @return [String, nil] The URL or nil
115
115
  def link_url(link)
116
116
  # External links use id attribute (relationship ID) or target
117
- link.respond_to?(:target) ? link.target : link.id
117
+ link.is_a?(Lutaml::Model::Serializable) && link.class.attributes.key?(:target) ? link.target : link.id
118
118
  end
119
119
 
120
120
  # Validate URL format
@@ -51,7 +51,7 @@ module Uniword
51
51
  next if @allow_direct_formatting
52
52
 
53
53
  para.runs.each_with_index do |run, run_index|
54
- next unless run.respond_to?(:properties)
54
+ next unless run.is_a?(Uniword::Wordprocessingml::Run)
55
55
  next unless run.properties
56
56
 
57
57
  next unless has_direct_run_formatting?(run)
@@ -56,7 +56,7 @@ module Uniword
56
56
  return false if table.rows.empty?
57
57
 
58
58
  first_row = table.rows.first
59
- return false unless first_row.respond_to?(:cells)
59
+ return false unless first_row.is_a?(Uniword::Wordprocessingml::TableRow)
60
60
 
61
61
  # Check if first row cells have header properties
62
62
  # In OOXML, header rows typically have specific table cell properties
@@ -42,7 +42,7 @@ module Uniword
42
42
  color_scheme.dup.tap do |scheme|
43
43
  # Transform each color in the scheme
44
44
  scheme.colors&.each_value do |color|
45
- next unless color.respond_to?(:val) && color.val&.match?(/^[0-9A-Fa-f]{6}$/)
45
+ next unless color.is_a?(Uniword::Drawingml::SrgbColor) && color.val&.match?(/^[0-9A-Fa-f]{6}$/)
46
46
 
47
47
  color.val = shift_color(
48
48
  color.val,
@@ -93,7 +93,7 @@ module Uniword
93
93
 
94
94
  def bundled_path(subdir)
95
95
  # Use Gem.datadir if available, otherwise fall back to data/ directory
96
- data_path = (Gem.datadir("uniword") if defined?(Gem) && Gem.respond_to?(:datadir))
96
+ data_path = (Gem.datadir("uniword") if defined?(Gem) && Gem.is_a?(Module))
97
97
  data_path ||= File.expand_path("../../../data", __dir__)
98
98
  File.join(data_path, subdir)
99
99
  end
@@ -87,7 +87,7 @@ lightness_shift: nil)
87
87
  fol_hlink]
88
88
 
89
89
  color_attrs.each do |attr|
90
- color_obj = color_scheme.send(attr)
90
+ color_obj = color_scheme.public_send(attr)
91
91
  next unless color_obj&.srgb_clr&.val
92
92
 
93
93
  original = color_obj.srgb_clr.val
@@ -66,7 +66,7 @@ module Uniword
66
66
  uniword_slug = detect_ms_theme(word_theme)
67
67
 
68
68
  # Fall back to detection by theme name
69
- uniword_slug = from_ms_name(word_theme.name) if uniword_slug.nil? && word_theme.respond_to?(:name) && word_theme.name
69
+ uniword_slug = from_ms_name(word_theme.name) if uniword_slug.nil? && word_theme.is_a?(Uniword::Drawingml::Theme) && word_theme.name
70
70
 
71
71
  return nil unless uniword_slug
72
72
 
@@ -149,7 +149,7 @@ module Uniword
149
149
  #
150
150
  # @return [String] The character read
151
151
  def read_single_char
152
- if @input.respond_to?(:getch)
152
+ if @input.is_a?(IO)
153
153
  @input.getch
154
154
  else
155
155
  @input.gets&.chomp&.first || "q"
@@ -58,16 +58,16 @@ module Uniword
58
58
  def extract_text(document)
59
59
  parts = []
60
60
 
61
- paragraphs = if document.respond_to?(:paragraphs)
61
+ paragraphs = if document.is_a?(Uniword::Wordprocessingml::DocumentRoot) || document.is_a?(Uniword::Wordprocessingml::Body)
62
62
  document.paragraphs
63
63
  else
64
64
  []
65
65
  end
66
66
  paragraphs.each do |para|
67
- parts << para.text if para.respond_to?(:text)
67
+ parts << para.text if para.is_a?(Uniword::Wordprocessingml::Paragraph)
68
68
  end
69
69
 
70
- tables = if document.respond_to?(:tables)
70
+ tables = if document.is_a?(Uniword::Wordprocessingml::DocumentRoot)
71
71
  document.tables
72
72
  else
73
73
  []
@@ -85,13 +85,13 @@ module Uniword
85
85
  # @param parts [Array<String>] Accumulator for text parts
86
86
  # @return [void]
87
87
  def extract_table_text(table, parts)
88
- return unless table.respond_to?(:rows)
88
+ return unless table.is_a?(Uniword::Wordprocessingml::Table)
89
89
 
90
90
  table.rows.each do |row|
91
- next unless row.respond_to?(:cells)
91
+ next unless row.is_a?(Uniword::Wordprocessingml::TableRow)
92
92
 
93
93
  row.cells.each do |cell|
94
- parts << cell.text if cell.respond_to?(:text)
94
+ parts << cell.text if cell.is_a?(Uniword::Wordprocessingml::TableCell)
95
95
  end
96
96
  end
97
97
  end
@@ -140,7 +140,7 @@ module Uniword
140
140
  # - :rename - Keep both, rename imported styles
141
141
  # @return [void]
142
142
  def apply_to(document, strategy: :keep_existing)
143
- return unless document.respond_to?(:styles_configuration)
143
+ return unless document.is_a?(Uniword::Wordprocessingml::DocumentRoot)
144
144
 
145
145
  styles.each do |style|
146
146
  case strategy
@@ -97,10 +97,9 @@ module Uniword
97
97
  # @return [void]
98
98
  def remove_element(element, document)
99
99
  # Remove from paragraphs
100
- document.body.paragraphs.delete(element) if document.body.respond_to?(:paragraphs)
101
-
102
- # Clear document cache
103
- document.clear_element_cache if document.respond_to?(:clear_element_cache)
100
+ if document.body.is_a?(Uniword::Wordprocessingml::Body)
101
+ document.body.paragraphs.delete(element)
102
+ end
104
103
  end
105
104
  end
106
105
  end
@@ -65,7 +65,7 @@ module Uniword
65
65
  # @param format_string [String] Format string
66
66
  # @return [String] Formatted value
67
67
  def apply_format(value, format_string)
68
- if value.respond_to?(:strftime)
68
+ if value.is_a?(Date) || value.is_a?(Time)
69
69
  value.strftime(format_string)
70
70
  else
71
71
  value.to_s
@@ -103,7 +103,7 @@ module Uniword
103
103
  # Find and replace variables in element
104
104
  # This is simplified - full implementation would
105
105
  # recursively process all text nodes
106
- return unless element.respond_to?(:text)
106
+ return unless element.is_a?(Uniword::Wordprocessingml::Run)
107
107
 
108
108
  text = element.text
109
109
  # Simple variable replacement ({{var}})
@@ -33,7 +33,7 @@ module Uniword
33
33
  replace_cell(element, text)
34
34
  else
35
35
  # Try to treat as paragraph-like
36
- replace_paragraph(element, text) if element.respond_to?(:runs)
36
+ replace_paragraph(element, text) if element.is_a?(Uniword::Wordprocessingml::Paragraph)
37
37
  end
38
38
  end
39
39
 
@@ -62,7 +62,7 @@ module Uniword
62
62
  def parse_paragraphs(paragraphs)
63
63
  paragraphs.each_with_index do |para, index|
64
64
  # Check paragraph's own comments
65
- if para.respond_to?(:comments) && para.comments
65
+ if para.is_a?(Uniword::CommentsPart) && para.comments
66
66
  para.comments.each do |comment|
67
67
  marker = parse_comment_text(comment.text, para, index)
68
68
  @markers << marker if marker
@@ -70,10 +70,10 @@ module Uniword
70
70
  end
71
71
 
72
72
  # Check run comments
73
- next unless para.respond_to?(:runs)
73
+ next unless para.is_a?(Uniword::Wordprocessingml::Paragraph)
74
74
 
75
75
  para.runs.each do |run|
76
- next unless run.respond_to?(:comments) && run.comments
76
+ next unless run.is_a?(Uniword::CommentsPart) && run.comments
77
77
 
78
78
  run.comments.each do |comment|
79
79
  marker = parse_comment_text(comment.text, run, index)
@@ -93,13 +93,13 @@ module Uniword
93
93
  base_position = @document.paragraphs.size + (table_index * 100)
94
94
 
95
95
  # Parse table rows
96
- next unless table.respond_to?(:rows)
96
+ next unless table.is_a?(Uniword::Wordprocessingml::Table)
97
97
 
98
98
  table.rows.each_with_index do |row, row_index|
99
99
  row_position = base_position + (row_index * 10)
100
100
 
101
101
  # Check row comments
102
- if row.respond_to?(:comments) && row.comments
102
+ if row.is_a?(Uniword::CommentsPart) && row.comments
103
103
  row.comments.each do |comment|
104
104
  marker = parse_comment_text(comment.text, row, row_position)
105
105
  @markers << marker if marker
@@ -107,11 +107,13 @@ module Uniword
107
107
  end
108
108
 
109
109
  # Parse cells
110
- next unless row.respond_to?(:cells)
110
+ next unless row.is_a?(Uniword::Wordprocessingml::TableRow)
111
111
 
112
112
  row.cells.each_with_index do |cell, _cell_index|
113
113
  # Parse cell paragraphs
114
- parse_paragraphs(cell.paragraphs) if cell.respond_to?(:paragraphs)
114
+ if cell.is_a?(Uniword::Wordprocessingml::TableCell)
115
+ parse_paragraphs(cell.paragraphs)
116
+ end
115
117
  end
116
118
  end
117
119
  end