uniword 1.0.4 → 1.0.5

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 +3 -3
  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
@@ -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
@@ -178,10 +178,10 @@ module Uniword
178
178
  def remove_template_comments(document)
179
179
  # Remove comments from paragraphs
180
180
  document.paragraphs.each do |para|
181
- next unless para.respond_to?(:comments)
181
+ next unless para.is_a?(Uniword::CommentsPart)
182
182
 
183
183
  # Filter out template comments
184
- next unless para.respond_to?(:attached_comments)
184
+ next unless para.is_a?(Uniword::CommentsPart)
185
185
 
186
186
  para.attached_comments.reject! do |c|
187
187
  template_comment?(c)
@@ -194,7 +194,7 @@ module Uniword
194
194
  # @param comment [Comment] Comment to check
195
195
  # @return [Boolean] true if template comment
196
196
  def template_comment?(comment)
197
- return false unless comment.respond_to?(:text)
197
+ return false unless comment.is_a?(Uniword::Comment)
198
198
 
199
199
  text = comment.text
200
200
  text.match?(/^\{\{.+\}\}$/)
@@ -108,9 +108,8 @@ module Uniword
108
108
  # For hashes, use key access
109
109
  if object.is_a?(Hash)
110
110
  object[property.to_sym] || object[property]
111
- # For objects with methods, call the method
112
- elsif object.respond_to?(property.to_sym)
113
- object.send(property.to_sym)
111
+ else
112
+ object.public_send(property.to_sym)
114
113
  end
115
114
  end
116
115
 
@@ -176,7 +175,7 @@ module Uniword
176
175
 
177
176
  return false if left_num.nil? || right_num.nil?
178
177
 
179
- left_num.send(operator, right_num)
178
+ left_num.public_send(operator, right_num)
180
179
  end
181
180
 
182
181
  # Convert value to number
@@ -185,8 +184,8 @@ module Uniword
185
184
  # @return [Numeric, nil] Numeric value or nil
186
185
  def to_number(value)
187
186
  return value if value.is_a?(Numeric)
188
- return value.to_i if value.respond_to?(:to_i) && value.to_s.match?(/^\d+$/)
189
- return value.to_f if value.respond_to?(:to_f) && value.to_s.match?(/^\d+\.\d+$/)
187
+ return value.to_i if value.to_s.match?(/^\d+$/)
188
+ return value.to_f if value.to_s.match?(/^\d+\.\d+$/)
190
189
 
191
190
  nil
192
191
  end
@@ -182,7 +182,7 @@ module Uniword
182
182
 
183
183
  colors = {}
184
184
  COLOR_KEYS.each do |key|
185
- if (color_ref = word_colors.send(key))
185
+ if (color_ref = word_colors.public_send(key))
186
186
  colors[key] = extract_hex_color(color_ref)
187
187
  end
188
188
  end
@@ -145,7 +145,7 @@ module Uniword
145
145
  style_ref = paragraph.properties&.style
146
146
  return nil unless style_ref
147
147
 
148
- if style_ref.respond_to?(:value)
148
+ if style_ref.is_a?(Uniword::Properties::StyleReference)
149
149
  style_ref.value
150
150
  else
151
151
  style_ref.to_s
@@ -239,7 +239,7 @@ module Uniword
239
239
  result = %(<span style="font-size:#{size_pt}pt">#{result}</span>)
240
240
  end
241
241
 
242
- if props.font.respond_to?(:ascii) && props.font.ascii
242
+ if props.font.is_a?(Uniword::Properties::RunFonts) && props.font.ascii
243
243
  result = %(<span style="font-family:'#{props.font.ascii}'">#{result}</span>)
244
244
  elsif props.font.is_a?(String) && !props.font.empty?
245
245
  result = %(<span style="font-family:'#{props.font}'">#{result}</span>)
@@ -582,7 +582,7 @@ module Uniword
582
582
 
583
583
  attrs = []
584
584
 
585
- attrs << %(w:id="#{props.id.value}") if props.id.respond_to?(:value) && props.id.value
585
+ attrs << %(w:id="#{props.id.value}") if props.id&.value
586
586
 
587
587
  attrs << 'w:showingPlcHdr="t"' if props.showing_placeholder_header
588
588
 
@@ -590,14 +590,12 @@ module Uniword
590
590
 
591
591
  if props.placeholder&.doc_part
592
592
  doc_part = props.placeholder.doc_part
593
- attrs << %(w:docPart="#{doc_part.value}") if doc_part.respond_to?(:value) && doc_part.value
593
+ attrs << %(w:docPart="#{doc_part.value}") if doc_part&.value
594
594
  end
595
595
 
596
- attrs << %(w:text="#{props.text.value}") if props.text.respond_to?(:value) && props.text.value
596
+ attrs << %(w:tag="#{escape_xml(props.tag.value)}") if props.tag&.value
597
597
 
598
- attrs << %(w:tag="#{escape_xml(props.tag.value)}") if props.tag.respond_to?(:value) && props.tag.value
599
-
600
- attrs << %(w:alias="#{escape_xml(props.alias_name.value)}") if props.alias_name.respond_to?(:value) && props.alias_name.value
598
+ attrs << %(w:alias="#{escape_xml(props.alias_name.value)}") if props.alias_name&.value
601
599
 
602
600
  attrs.empty? ? "" : " #{attrs.join(' ')}"
603
601
  end