uniword 1.2.5 → 1.3.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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +223 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/config/ooxml/schemas/shared_types.yml +5 -4
  5. data/data/schemas/ecma/dc.xsd +118 -0
  6. data/data/schemas/ecma/dcmitype.xsd +50 -0
  7. data/data/schemas/ecma/dcterms.xsd +331 -0
  8. data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
  9. data/data/schemas/ecma/xml.xsd +117 -0
  10. data/lib/uniword/builder/bibliography_builder.rb +1 -1
  11. data/lib/uniword/builder/chart_builder.rb +19 -9
  12. data/lib/uniword/builder/comment_anchorer.rb +221 -0
  13. data/lib/uniword/builder/document_builder.rb +97 -6
  14. data/lib/uniword/builder/image_builder.rb +6 -5
  15. data/lib/uniword/builder/run_utils.rb +2 -0
  16. data/lib/uniword/builder.rb +1 -0
  17. data/lib/uniword/cli/fonts_cli.rb +52 -0
  18. data/lib/uniword/cli/main.rb +110 -11
  19. data/lib/uniword/cli/page_cli.rb +85 -0
  20. data/lib/uniword/cli/styles_cli.rb +204 -0
  21. data/lib/uniword/cli/theme_cli.rb +88 -0
  22. data/lib/uniword/cli/toc_cli.rb +22 -2
  23. data/lib/uniword/comment.rb +20 -15
  24. data/lib/uniword/comment_range.rb +3 -3
  25. data/lib/uniword/comments_part.rb +51 -19
  26. data/lib/uniword/configuration/configuration_loader.rb +5 -3
  27. data/lib/uniword/configuration.rb +93 -1
  28. data/lib/uniword/content_types.rb +19 -39
  29. data/lib/uniword/document_factory.rb +9 -1
  30. data/lib/uniword/document_writer.rb +16 -5
  31. data/lib/uniword/docx/chart_part.rb +34 -0
  32. data/lib/uniword/docx/custom_xml_item.rb +83 -0
  33. data/lib/uniword/docx/header_footer_part.rb +140 -0
  34. data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
  35. data/lib/uniword/docx/header_footer_view.rb +225 -0
  36. data/lib/uniword/docx/id_allocator.rb +130 -36
  37. data/lib/uniword/docx/package.rb +223 -35
  38. data/lib/uniword/docx/package_defaults.rb +58 -78
  39. data/lib/uniword/docx/package_integrity_checker.rb +312 -0
  40. data/lib/uniword/docx/package_serialization.rb +103 -231
  41. data/lib/uniword/docx/part.rb +106 -0
  42. data/lib/uniword/docx/part_collection.rb +123 -0
  43. data/lib/uniword/docx/reconciler/body.rb +59 -68
  44. data/lib/uniword/docx/reconciler/fix.rb +46 -0
  45. data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
  46. data/lib/uniword/docx/reconciler/helpers.rb +9 -1
  47. data/lib/uniword/docx/reconciler/notes.rb +17 -8
  48. data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
  49. data/lib/uniword/docx/reconciler/parts.rb +36 -13
  50. data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
  51. data/lib/uniword/docx/reconciler/tables.rb +78 -16
  52. data/lib/uniword/docx/reconciler/theme.rb +7 -4
  53. data/lib/uniword/docx/reconciler.rb +36 -14
  54. data/lib/uniword/docx.rb +10 -0
  55. data/lib/uniword/drawingml/blip.rb +0 -1
  56. data/lib/uniword/drawingml/color_scheme.rb +5 -6
  57. data/lib/uniword/drawingml/font_scheme.rb +4 -8
  58. data/lib/uniword/drawingml/theme.rb +12 -6
  59. data/lib/uniword/errors.rb +8 -1
  60. data/lib/uniword/footer.rb +4 -0
  61. data/lib/uniword/header.rb +4 -0
  62. data/lib/uniword/hyperlink.rb +1 -1
  63. data/lib/uniword/image.rb +6 -6
  64. data/lib/uniword/lazy_loader.rb +1 -2
  65. data/lib/uniword/model_attribute_access.rb +20 -4
  66. data/lib/uniword/ooxml/dotx_package.rb +42 -2
  67. data/lib/uniword/ooxml/part_definition.rb +171 -0
  68. data/lib/uniword/ooxml/part_registry.rb +311 -0
  69. data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
  70. data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
  71. data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
  72. data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
  73. data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
  74. data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
  75. data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
  76. data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
  77. data/lib/uniword/ooxml/types.rb +10 -0
  78. data/lib/uniword/ooxml.rb +2 -0
  79. data/lib/uniword/properties/alignment.rb +8 -3
  80. data/lib/uniword/properties/border.rb +52 -6
  81. data/lib/uniword/properties/cell_vertical_align.rb +4 -1
  82. data/lib/uniword/properties/color_value.rb +8 -3
  83. data/lib/uniword/properties/highlight.rb +9 -5
  84. data/lib/uniword/properties/shading.rb +4 -3
  85. data/lib/uniword/properties/tab_stop.rb +6 -1
  86. data/lib/uniword/properties/table_justification.rb +4 -1
  87. data/lib/uniword/properties/underline.rb +4 -2
  88. data/lib/uniword/properties/vertical_align.rb +6 -3
  89. data/lib/uniword/review/review_manager.rb +23 -9
  90. data/lib/uniword/revision.rb +9 -9
  91. data/lib/uniword/schema/model_generator.rb +31 -15
  92. data/lib/uniword/shared_types/hex_color.rb +4 -1
  93. data/lib/uniword/shared_types/pixel_measure.rb +4 -1
  94. data/lib/uniword/shared_types/point_measure.rb +4 -1
  95. data/lib/uniword/shared_types/text_alignment.rb +6 -1
  96. data/lib/uniword/shared_types/twips_measure.rb +4 -1
  97. data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
  98. data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
  99. data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
  100. data/lib/uniword/template/variable_resolver.rb +40 -7
  101. data/lib/uniword/themes/theme_transformation.rb +55 -41
  102. data/lib/uniword/toc/toc_generator.rb +2 -0
  103. data/lib/uniword/validation/engine.rb +35 -0
  104. data/lib/uniword/validation/link_checker.rb +1 -1
  105. data/lib/uniword/validation/opc_validator.rb +0 -1
  106. data/lib/uniword/validation/report/layer_result.rb +0 -1
  107. data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
  108. data/lib/uniword/validation/report/verification_report.rb +0 -2
  109. data/lib/uniword/validation/report.rb +20 -0
  110. data/lib/uniword/validation/rules/base.rb +10 -2
  111. data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
  112. data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
  113. data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
  114. data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
  115. data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
  116. data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
  117. data/lib/uniword/validation/rules/document_context.rb +7 -0
  118. data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
  119. data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
  120. data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
  121. data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
  122. data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
  123. data/lib/uniword/validation/rules/images_rule.rb +0 -2
  124. data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
  125. data/lib/uniword/validation/rules/model_context.rb +32 -0
  126. data/lib/uniword/validation/rules/model_rule.rb +34 -0
  127. data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
  128. data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
  129. data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
  130. data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
  131. data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
  132. data/lib/uniword/validation/rules/settings_rule.rb +0 -2
  133. data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
  134. data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
  135. data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
  136. data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
  137. data/lib/uniword/validation/rules/tables_rule.rb +0 -2
  138. data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
  139. data/lib/uniword/validation/rules/theme_rule.rb +0 -2
  140. data/lib/uniword/validation/rules.rb +61 -23
  141. data/lib/uniword/validation/schema_registry.rb +11 -0
  142. data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
  143. data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
  144. data/lib/uniword/validation/validators.rb +0 -9
  145. data/lib/uniword/validation/verify_orchestrator.rb +0 -7
  146. data/lib/uniword/validation.rb +7 -2
  147. data/lib/uniword/version.rb +1 -1
  148. data/lib/uniword/vml/imagedata.rb +0 -1
  149. data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
  150. data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
  151. data/lib/uniword/wordprocessingml/document_root.rb +211 -21
  152. data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
  153. data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
  154. data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
  155. data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
  156. data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
  157. data/lib/uniword/wordprocessingml/level.rb +6 -20
  158. data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
  159. data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
  160. data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
  161. data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
  162. data/lib/uniword/wordprocessingml/rsids.rb +0 -2
  163. data/lib/uniword/wordprocessingml/run.rb +3 -0
  164. data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
  165. data/lib/uniword/wordprocessingml/settings.rb +8 -34
  166. data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
  167. data/lib/uniword/wordprocessingml/style.rb +2 -1
  168. data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
  169. data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
  170. data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
  171. data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
  172. data/lib/uniword/wordprocessingml.rb +4 -0
  173. data/lib/uniword.rb +27 -2
  174. metadata +38 -19
  175. data/config/validation_rules.yml +0 -60
  176. data/config/warning_rules.yml +0 -57
  177. data/lib/uniword/validation/document_validator.rb +0 -272
  178. data/lib/uniword/validation/structural_validator.rb +0 -116
  179. data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
  180. data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
  181. data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
  182. data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
  183. data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
  184. data/lib/uniword/validators/element_validator.rb +0 -93
  185. data/lib/uniword/validators/paragraph_validator.rb +0 -116
  186. data/lib/uniword/validators/table_validator.rb +0 -134
  187. data/lib/uniword/validators.rb +0 -9
  188. data/lib/uniword/warnings/warning.rb +0 -130
  189. data/lib/uniword/warnings/warning_collector.rb +0 -234
  190. data/lib/uniword/warnings/warning_report.rb +0 -159
  191. data/lib/uniword/warnings.rb +0 -9
@@ -1,134 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # NOTE: element_validator is already loaded by uniword.rb
4
- # No need to require it here to avoid circular dependencies
5
-
6
- module Uniword
7
- module Validators
8
- # Validator for Table elements
9
- # Responsibility: Validate table-specific constraints
10
- #
11
- # A valid table:
12
- # - Must be a Table instance
13
- # - Can have zero or more rows (empty tables are valid)
14
- # - All rows must be valid TableRow instances
15
- # - All rows should have consistent column counts (warning, not error)
16
- # - Properties, if present, must be valid TableProperties
17
- #
18
- # @example Using the table validator
19
- # validator = Uniword::Validators::TableValidator.new
20
- # validator.valid?(table) # => true or false
21
- # validator.errors(table) # => ["error message", ...]
22
- class TableValidator < ElementValidator
23
- # Validate a table element
24
- #
25
- # @param element [Table] The table to validate
26
- # @return [Boolean] true if valid, false otherwise
27
- def valid?(element)
28
- return false unless super
29
- return false unless element.is_a?(Uniword::Wordprocessingml::Table)
30
-
31
- validate_rows(element) &&
32
- validate_properties(element)
33
- end
34
-
35
- # Get validation errors for a table
36
- #
37
- # @param element [Table] The table to validate
38
- # @return [Array<String>] Array of error messages
39
- def errors(element)
40
- errors = []
41
-
42
- # Check if element is nil
43
- return ["Element is nil"] if element.nil?
44
-
45
- # Check table type first (more specific than base check)
46
- return ["Element must be a Table"] unless element.is_a?(Uniword::Wordprocessingml::Table)
47
-
48
- # Validate rows - collect all specific errors
49
- errors.concat(row_errors(element))
50
-
51
- # Validate properties - collect all specific errors
52
- errors.concat(property_errors(element))
53
-
54
- errors
55
- end
56
-
57
- # Get validation warnings (non-critical issues)
58
- #
59
- # @param element [Table] The table to validate
60
- # @return [Array<String>] Array of warning messages
61
- def warnings(element)
62
- warnings = []
63
-
64
- return warnings unless element.is_a?(Uniword::Wordprocessingml::Table)
65
- return warnings if element.rows.empty?
66
-
67
- # Check for inconsistent column counts
68
- column_counts = element.rows.map { |row| row.cells.count }.uniq
69
- warnings << "Table has inconsistent column counts: #{column_counts.join(', ')}" if column_counts.size > 1
70
-
71
- warnings
72
- end
73
-
74
- private
75
-
76
- # Validate that all rows are valid TableRow instances
77
- #
78
- # @param table [Table] The table to validate
79
- # @return [Boolean] true if all rows are valid
80
- def validate_rows(table)
81
- return true if table.rows.nil? || table.rows.empty?
82
-
83
- table.rows.all?(Uniword::Wordprocessingml::TableRow)
84
- end
85
-
86
- # Get errors related to rows
87
- #
88
- # @param table [Table] The table to validate
89
- # @return [Array<String>] Array of error messages
90
- def row_errors(table)
91
- errors = []
92
-
93
- return errors if table.rows.nil? || table.rows.empty?
94
-
95
- table.rows.each_with_index do |row, index|
96
- errors << "Row at index #{index} must be a TableRow instance" unless row.is_a?(Uniword::Wordprocessingml::TableRow)
97
- end
98
-
99
- errors
100
- end
101
-
102
- # Validate properties if present
103
- #
104
- # @param table [Table] The table to validate
105
- # @return [Boolean] true if properties are valid or nil
106
- def validate_properties(table)
107
- return true if table.properties.nil?
108
-
109
- table.properties.is_a?(Uniword::Wordprocessingml::TableProperties)
110
- end
111
-
112
- # Get errors related to properties
113
- #
114
- # @param table [Table] The table to validate
115
- # @return [Array<String>] Array of error messages
116
- def property_errors(table)
117
- return [] if table.properties.nil?
118
-
119
- return ["Properties must be a TableProperties instance"] unless table.properties.is_a?(Uniword::Wordprocessingml::TableProperties)
120
-
121
- []
122
- end
123
- end
124
- end
125
- end
126
-
127
- # Register this validator when the file is loaded
128
- # This ensures the validator is available regardless of load order
129
- if defined?(Uniword::Validators::ElementValidator)
130
- Uniword::Validators::ElementValidator.register(
131
- Uniword::Wordprocessingml::Table,
132
- Uniword::Validators::TableValidator,
133
- )
134
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Uniword
4
- module Validators
5
- autoload :ElementValidator, "#{__dir__}/validators/element_validator"
6
- autoload :ParagraphValidator, "#{__dir__}/validators/paragraph_validator"
7
- autoload :TableValidator, "#{__dir__}/validators/table_validator"
8
- end
9
- end
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Uniword
4
- module Warnings
5
- # Represents a single warning about an unsupported element or feature.
6
- #
7
- # Responsibility: Store information about one warning.
8
- # Single Responsibility: Only represents warning data.
9
- #
10
- # A warning includes:
11
- # - Type (unsupported_element, unsupported_attribute, etc.)
12
- # - Severity (error, warning, info)
13
- # - Element/attribute name
14
- # - Context where it was encountered
15
- # - Location in document
16
- # - Suggestion for user
17
- #
18
- # @example Create a warning
19
- # warning = Warning.new(
20
- # type: :unsupported_element,
21
- # severity: :warning,
22
- # element: 'chart',
23
- # message: 'Unsupported element: chart',
24
- # context: 'While parsing paragraph',
25
- # suggestion: 'Chart will be preserved but not editable'
26
- # )
27
- class Warning
28
- # @return [Symbol] Type of warning
29
- attr_reader :type
30
-
31
- # @return [Symbol] Severity level (:error, :warning, :info)
32
- attr_reader :severity
33
-
34
- # @return [String] Element name
35
- attr_reader :element
36
-
37
- # @return [String, nil] Attribute name (for attribute warnings)
38
- attr_reader :attribute
39
-
40
- # @return [String] Warning message
41
- attr_reader :message
42
-
43
- # @return [String, nil] Context where warning occurred
44
- attr_reader :context
45
-
46
- # @return [String, nil] Location in document
47
- attr_reader :location
48
-
49
- # @return [String, nil] Suggestion for user
50
- attr_reader :suggestion
51
-
52
- # Initialize a new warning.
53
- #
54
- # @param attributes [Hash] Warning attributes
55
- # @option attributes [Symbol] :type Warning type
56
- # @option attributes [Symbol] :severity Severity level
57
- # @option attributes [String] :element Element name
58
- # @option attributes [String] :attribute Attribute name (optional)
59
- # @option attributes [String] :message Warning message
60
- # @option attributes [String] :context Context (optional)
61
- # @option attributes [String] :location Location (optional)
62
- # @option attributes [String] :suggestion Suggestion (optional)
63
- #
64
- # @example Create a warning
65
- # warning = Warning.new(
66
- # type: :unsupported_element,
67
- # severity: :error,
68
- # element: 'chart',
69
- # message: 'Unsupported element: chart'
70
- # )
71
- def initialize(attributes)
72
- @type = attributes[:type]
73
- @severity = attributes[:severity]
74
- @element = attributes[:element]
75
- @attribute = attributes[:attribute]
76
- @message = attributes[:message]
77
- @context = attributes[:context]
78
- @location = attributes[:location]
79
- @suggestion = attributes[:suggestion]
80
- end
81
-
82
- # Check if this is an error-level warning.
83
- #
84
- # @return [Boolean] true if severity is :error
85
- def error?
86
- @severity == :error
87
- end
88
-
89
- # Check if this is a warning-level warning.
90
- #
91
- # @return [Boolean] true if severity is :warning
92
- def warning?
93
- @severity == :warning
94
- end
95
-
96
- # Check if this is an info-level warning.
97
- #
98
- # @return [Boolean] true if severity is :info
99
- def info?
100
- @severity == :info
101
- end
102
-
103
- # Convert to hash representation.
104
- #
105
- # @return [Hash] Hash representation
106
- def to_h
107
- {
108
- type: @type,
109
- severity: @severity,
110
- element: @element,
111
- attribute: @attribute,
112
- message: @message,
113
- context: @context,
114
- location: @location,
115
- suggestion: @suggestion,
116
- }.compact
117
- end
118
-
119
- # Convert to string for display.
120
- #
121
- # @return [String] String representation
122
- def to_s
123
- severity_str = @severity.to_s.upcase
124
- element_str = @attribute ? "#{@element}/@#{@attribute}" : @element
125
- msg_str = @message
126
- "[#{severity_str}] #{element_str}: #{msg_str}"
127
- end
128
- end
129
- end
130
- end
@@ -1,234 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Uniword
4
- module Warnings
5
- # Collects warnings during document processing.
6
- #
7
- # Responsibility: Track unsupported elements and features.
8
- # Single Responsibility: Only collects warnings.
9
- #
10
- # This class is used during deserialization to track all unsupported
11
- # elements and attributes encountered, allowing users to understand
12
- # what features may be lost in round-trip operations.
13
- #
14
- # External configuration: config/warning_rules.yml
15
- #
16
- # @example Use during deserialization
17
- # collector = WarningCollector.new
18
- # collector.record_unsupported('chart', context: 'In paragraph 5')
19
- # report = collector.report
20
- # puts report.summary
21
- class WarningCollector
22
- # @return [Array<Warning>] Collected warnings
23
- attr_reader :warnings
24
-
25
- # @return [Hash<String, Integer>] Element occurrence counts
26
- attr_reader :element_counts
27
-
28
- # Initialize a new warning collector.
29
- #
30
- # @param config_file [String] Path to configuration file
31
- #
32
- # @example Create with default config
33
- # collector = WarningCollector.new
34
- #
35
- # @example Create with custom config
36
- # collector = WarningCollector.new(
37
- # config_file: 'custom_warnings.yml'
38
- # )
39
- def initialize(config_file: nil)
40
- @config = load_config(config_file)
41
- @warnings = []
42
- @element_counts = Hash.new(0)
43
- end
44
-
45
- # Record an unsupported element.
46
- #
47
- # @param element_tag [String] Element tag name
48
- # @param context [String] Context where element was found
49
- # @param location [String] Location in document (optional)
50
- # @return [void]
51
- #
52
- # @example Record unsupported element
53
- # collector.record_unsupported(
54
- # 'chart',
55
- # context: 'While parsing paragraph',
56
- # location: '/document/body/p[5]'
57
- # )
58
- def record_unsupported(element_tag, context:, location: nil)
59
- return unless enabled?
60
- return if max_warnings_reached?
61
-
62
- @element_counts[element_tag] += 1
63
-
64
- warning = Warning.new(
65
- type: :unsupported_element,
66
- severity: determine_severity(element_tag),
67
- element: element_tag,
68
- message: "Unsupported element: #{element_tag}",
69
- context: context,
70
- location: location,
71
- suggestion: get_suggestion(element_tag),
72
- )
73
-
74
- @warnings << warning
75
- log_warning(warning) if should_log?
76
- end
77
-
78
- # Record an unsupported attribute.
79
- #
80
- # @param element_tag [String] Element tag name
81
- # @param attribute_name [String] Attribute name
82
- # @param context [String] Context where attribute was found
83
- # @return [void]
84
- #
85
- # @example Record unsupported attribute
86
- # collector.record_unsupported_attribute(
87
- # 'w:p',
88
- # 'customAttr',
89
- # context: 'In paragraph properties'
90
- # )
91
- def record_unsupported_attribute(element_tag, attribute_name, context:)
92
- return unless enabled?
93
- return if max_warnings_reached?
94
-
95
- warning = Warning.new(
96
- type: :unsupported_attribute,
97
- severity: :info,
98
- element: element_tag,
99
- attribute: attribute_name,
100
- message: "Unsupported attribute: #{element_tag}/@#{attribute_name}",
101
- context: context,
102
- )
103
-
104
- @warnings << warning
105
- log_warning(warning) if should_log?
106
- end
107
-
108
- # Generate warning report.
109
- #
110
- # @return [WarningReport] Aggregated warning report
111
- #
112
- # @example Get report
113
- # report = collector.report
114
- # puts "Total warnings: #{report.total_count}"
115
- def report
116
- WarningReport.new(
117
- warnings: @warnings,
118
- element_counts: @element_counts,
119
- config: @config,
120
- )
121
- end
122
-
123
- # Check if warnings were collected.
124
- #
125
- # @return [Boolean] true if warnings exist
126
- def any?
127
- @warnings.any?
128
- end
129
-
130
- # Clear all collected warnings.
131
- #
132
- # @return [void]
133
- def clear
134
- @warnings.clear
135
- @element_counts.clear
136
- end
137
-
138
- private
139
-
140
- def load_config(config_file)
141
- if config_file
142
- Configuration::ConfigurationLoader.load_file(config_file)
143
- else
144
- # Try to load default config
145
- default_path = File.join(
146
- Configuration::ConfigurationLoader::CONFIG_DIR,
147
- "warning_rules.yml",
148
- )
149
-
150
- if File.exist?(default_path)
151
- Configuration::ConfigurationLoader.load("warning_rules")
152
- else
153
- # Use empty config with defaults
154
- default_config
155
- end
156
- end
157
- rescue Configuration::ConfigurationError
158
- # If config fails to load, use defaults
159
- default_config
160
- end
161
-
162
- def default_config
163
- {
164
- warning_system: {
165
- enabled: true,
166
- log_warnings: false,
167
- log_level: :warn,
168
- critical_elements: [],
169
- warning_elements: [],
170
- element_suggestions: {},
171
- reporting: {
172
- max_warnings: 100,
173
- },
174
- },
175
- }
176
- end
177
-
178
- def warning_config
179
- @config[:warning_system] || {}
180
- end
181
-
182
- def enabled?
183
- warning_config[:enabled] != false
184
- end
185
-
186
- def should_log?
187
- warning_config[:log_warnings] == true
188
- end
189
-
190
- def max_warnings_reached?
191
- max = warning_config.dig(:reporting, :max_warnings) || 100
192
- @warnings.count >= max
193
- end
194
-
195
- def determine_severity(element_tag)
196
- critical = warning_config[:critical_elements] || []
197
- warning_els = warning_config[:warning_elements] || []
198
-
199
- if critical.include?(element_tag)
200
- :error
201
- elsif warning_els.include?(element_tag)
202
- :warning
203
- else
204
- :info
205
- end
206
- end
207
-
208
- def get_suggestion(element_tag)
209
- suggestions = warning_config[:element_suggestions] || {}
210
- suggestions[element_tag] ||
211
- suggestions[element_tag.to_sym] ||
212
- "This element is not yet supported. Data may be lost in round-trip."
213
- end
214
-
215
- def log_warning(warning)
216
- level = warning_config[:log_level] || :warn
217
- message = "[WARNING] #{warning}"
218
-
219
- # Use Uniword logger if available
220
- if defined?(Uniword::Logger)
221
- case level
222
- when :debug then Uniword::Logger.debug(message)
223
- when :info then Uniword::Logger.info(message)
224
- when :warn then Uniword::Logger.warn(message)
225
- when :error then Uniword::Logger.error(message)
226
- end
227
- elsif %i[warn error].include?(level)
228
- # Fall back to standard output
229
- puts message
230
- end
231
- end
232
- end
233
- end
234
- end
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Uniword
6
- module Warnings
7
- # Aggregated warning report.
8
- #
9
- # Responsibility: Aggregate and report warnings.
10
- # Single Responsibility: Only manages warning reporting.
11
- #
12
- # A warning report includes:
13
- # - Collection of all warnings
14
- # - Element occurrence counts
15
- # - Grouping by severity
16
- # - Summary statistics
17
- #
18
- # @example Create a report
19
- # report = WarningReport.new(
20
- # warnings: warnings,
21
- # element_counts: { 'chart' => 5, 'smartArt' => 2 }
22
- # )
23
- # puts report.summary
24
- class WarningReport
25
- # @return [Array<Warning>] All warnings
26
- attr_reader :warnings
27
-
28
- # @return [Hash<String, Integer>] Element occurrence counts
29
- attr_reader :element_counts
30
-
31
- # Initialize a new warning report.
32
- #
33
- # @param warnings [Array<Warning>] Warnings to include
34
- # @param element_counts [Hash<String, Integer>] Element counts
35
- # @param config [Hash] Configuration
36
- #
37
- # @example Create a report
38
- # report = WarningReport.new(
39
- # warnings: [warning1, warning2],
40
- # element_counts: { 'chart' => 3 }
41
- # )
42
- def initialize(warnings:, element_counts:, config: {})
43
- @warnings = warnings
44
- @element_counts = element_counts
45
- @config = config
46
- end
47
-
48
- # Check if report has any warnings.
49
- #
50
- # @return [Boolean] true if warnings exist
51
- def any?
52
- @warnings.any?
53
- end
54
-
55
- # Get error-level warnings.
56
- #
57
- # @return [Array<Warning>] Error warnings
58
- def errors
59
- @warnings.select(&:error?)
60
- end
61
-
62
- # Get warning-level warnings.
63
- #
64
- # @return [Array<Warning>] Warning-level warnings
65
- def warnings_only
66
- @warnings.select(&:warning?)
67
- end
68
-
69
- # Get info-level warnings.
70
- #
71
- # @return [Array<Warning>] Info warnings
72
- def infos
73
- @warnings.select(&:info?)
74
- end
75
-
76
- # Get count of error warnings.
77
- #
78
- # @return [Integer] Error count
79
- def error_count
80
- errors.count
81
- end
82
-
83
- # Get count of warning-level warnings.
84
- #
85
- # @return [Integer] Warning count
86
- def warning_count
87
- warnings_only.count
88
- end
89
-
90
- # Get count of info warnings.
91
- #
92
- # @return [Integer] Info count
93
- def info_count
94
- infos.count
95
- end
96
-
97
- # Get total warning count.
98
- #
99
- # @return [Integer] Total count
100
- def total_count
101
- @warnings.count
102
- end
103
-
104
- # Generate summary text.
105
- #
106
- # @return [String] Summary text
107
- def summary
108
- return "No warnings" unless any?
109
-
110
- lines = []
111
- lines << "Found #{total_count} warning(s):"
112
- lines << " Errors: #{error_count}"
113
- lines << " Warnings: #{warning_count}"
114
- lines << " Info: #{info_count}"
115
-
116
- if @element_counts.any?
117
- lines << ""
118
- lines << "Unsupported elements encountered:"
119
-
120
- @element_counts.sort_by { |_, count| -count }.each do |element, count|
121
- lines << " #{element}: #{count} occurrence(s)"
122
- end
123
- end
124
-
125
- lines.join("\n")
126
- end
127
-
128
- # Export to JSON string.
129
- #
130
- # @return [String] JSON representation
131
- def to_json(*_args)
132
- JSON.pretty_generate(to_h)
133
- end
134
-
135
- # Convert to hash representation.
136
- #
137
- # @return [Hash] Hash representation
138
- def to_h
139
- {
140
- total: total_count,
141
- by_severity: {
142
- errors: error_count,
143
- warnings: warning_count,
144
- infos: info_count,
145
- },
146
- element_counts: @element_counts,
147
- warnings: @warnings.map(&:to_h),
148
- }
149
- end
150
-
151
- # Convert to string for display.
152
- #
153
- # @return [String] String representation
154
- def to_s
155
- summary
156
- end
157
- end
158
- end
159
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Uniword
4
- module Warnings
5
- autoload :Warning, "#{__dir__}/warnings/warning"
6
- autoload :WarningCollector, "#{__dir__}/warnings/warning_collector"
7
- autoload :WarningReport, "#{__dir__}/warnings/warning_report"
8
- end
9
- end