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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +223 -0
- data/CONTRIBUTING.md +1 -1
- data/config/ooxml/schemas/shared_types.yml +5 -4
- data/data/schemas/ecma/dc.xsd +118 -0
- data/data/schemas/ecma/dcmitype.xsd +50 -0
- data/data/schemas/ecma/dcterms.xsd +331 -0
- data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
- data/data/schemas/ecma/xml.xsd +117 -0
- data/lib/uniword/builder/bibliography_builder.rb +1 -1
- data/lib/uniword/builder/chart_builder.rb +19 -9
- data/lib/uniword/builder/comment_anchorer.rb +221 -0
- data/lib/uniword/builder/document_builder.rb +97 -6
- data/lib/uniword/builder/image_builder.rb +6 -5
- data/lib/uniword/builder/run_utils.rb +2 -0
- data/lib/uniword/builder.rb +1 -0
- data/lib/uniword/cli/fonts_cli.rb +52 -0
- data/lib/uniword/cli/main.rb +110 -11
- data/lib/uniword/cli/page_cli.rb +85 -0
- data/lib/uniword/cli/styles_cli.rb +204 -0
- data/lib/uniword/cli/theme_cli.rb +88 -0
- data/lib/uniword/cli/toc_cli.rb +22 -2
- data/lib/uniword/comment.rb +20 -15
- data/lib/uniword/comment_range.rb +3 -3
- data/lib/uniword/comments_part.rb +51 -19
- data/lib/uniword/configuration/configuration_loader.rb +5 -3
- data/lib/uniword/configuration.rb +93 -1
- data/lib/uniword/content_types.rb +19 -39
- data/lib/uniword/document_factory.rb +9 -1
- data/lib/uniword/document_writer.rb +16 -5
- data/lib/uniword/docx/chart_part.rb +34 -0
- data/lib/uniword/docx/custom_xml_item.rb +83 -0
- data/lib/uniword/docx/header_footer_part.rb +140 -0
- data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
- data/lib/uniword/docx/header_footer_view.rb +225 -0
- data/lib/uniword/docx/id_allocator.rb +130 -36
- data/lib/uniword/docx/package.rb +223 -35
- data/lib/uniword/docx/package_defaults.rb +58 -78
- data/lib/uniword/docx/package_integrity_checker.rb +312 -0
- data/lib/uniword/docx/package_serialization.rb +103 -231
- data/lib/uniword/docx/part.rb +106 -0
- data/lib/uniword/docx/part_collection.rb +123 -0
- data/lib/uniword/docx/reconciler/body.rb +59 -68
- data/lib/uniword/docx/reconciler/fix.rb +46 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
- data/lib/uniword/docx/reconciler/helpers.rb +9 -1
- data/lib/uniword/docx/reconciler/notes.rb +17 -8
- data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
- data/lib/uniword/docx/reconciler/parts.rb +36 -13
- data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
- data/lib/uniword/docx/reconciler/tables.rb +78 -16
- data/lib/uniword/docx/reconciler/theme.rb +7 -4
- data/lib/uniword/docx/reconciler.rb +36 -14
- data/lib/uniword/docx.rb +10 -0
- data/lib/uniword/drawingml/blip.rb +0 -1
- data/lib/uniword/drawingml/color_scheme.rb +5 -6
- data/lib/uniword/drawingml/font_scheme.rb +4 -8
- data/lib/uniword/drawingml/theme.rb +12 -6
- data/lib/uniword/errors.rb +8 -1
- data/lib/uniword/footer.rb +4 -0
- data/lib/uniword/header.rb +4 -0
- data/lib/uniword/hyperlink.rb +1 -1
- data/lib/uniword/image.rb +6 -6
- data/lib/uniword/lazy_loader.rb +1 -2
- data/lib/uniword/model_attribute_access.rb +20 -4
- data/lib/uniword/ooxml/dotx_package.rb +42 -2
- data/lib/uniword/ooxml/part_definition.rb +171 -0
- data/lib/uniword/ooxml/part_registry.rb +311 -0
- data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
- data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
- data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
- data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
- data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
- data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
- data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
- data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
- data/lib/uniword/ooxml/types.rb +10 -0
- data/lib/uniword/ooxml.rb +2 -0
- data/lib/uniword/properties/alignment.rb +8 -3
- data/lib/uniword/properties/border.rb +52 -6
- data/lib/uniword/properties/cell_vertical_align.rb +4 -1
- data/lib/uniword/properties/color_value.rb +8 -3
- data/lib/uniword/properties/highlight.rb +9 -5
- data/lib/uniword/properties/shading.rb +4 -3
- data/lib/uniword/properties/tab_stop.rb +6 -1
- data/lib/uniword/properties/table_justification.rb +4 -1
- data/lib/uniword/properties/underline.rb +4 -2
- data/lib/uniword/properties/vertical_align.rb +6 -3
- data/lib/uniword/review/review_manager.rb +23 -9
- data/lib/uniword/revision.rb +9 -9
- data/lib/uniword/schema/model_generator.rb +31 -15
- data/lib/uniword/shared_types/hex_color.rb +4 -1
- data/lib/uniword/shared_types/pixel_measure.rb +4 -1
- data/lib/uniword/shared_types/point_measure.rb +4 -1
- data/lib/uniword/shared_types/text_alignment.rb +6 -1
- data/lib/uniword/shared_types/twips_measure.rb +4 -1
- data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
- data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
- data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
- data/lib/uniword/template/variable_resolver.rb +40 -7
- data/lib/uniword/themes/theme_transformation.rb +55 -41
- data/lib/uniword/toc/toc_generator.rb +2 -0
- data/lib/uniword/validation/engine.rb +35 -0
- data/lib/uniword/validation/link_checker.rb +1 -1
- data/lib/uniword/validation/opc_validator.rb +0 -1
- data/lib/uniword/validation/report/layer_result.rb +0 -1
- data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
- data/lib/uniword/validation/report/verification_report.rb +0 -2
- data/lib/uniword/validation/report.rb +20 -0
- data/lib/uniword/validation/rules/base.rb +10 -2
- data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
- data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
- data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
- data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
- data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
- data/lib/uniword/validation/rules/document_context.rb +7 -0
- data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
- data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
- data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
- data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
- data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
- data/lib/uniword/validation/rules/images_rule.rb +0 -2
- data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/model_context.rb +32 -0
- data/lib/uniword/validation/rules/model_rule.rb +34 -0
- data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
- data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
- data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
- data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
- data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
- data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
- data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
- data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
- data/lib/uniword/validation/rules/tables_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_rule.rb +0 -2
- data/lib/uniword/validation/rules.rb +61 -23
- data/lib/uniword/validation/schema_registry.rb +11 -0
- data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
- data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
- data/lib/uniword/validation/validators.rb +0 -9
- data/lib/uniword/validation/verify_orchestrator.rb +0 -7
- data/lib/uniword/validation.rb +7 -2
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/vml/imagedata.rb +0 -1
- data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
- data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +211 -21
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
- data/lib/uniword/wordprocessingml/level.rb +6 -20
- data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
- data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
- data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
- data/lib/uniword/wordprocessingml/rsids.rb +0 -2
- data/lib/uniword/wordprocessingml/run.rb +3 -0
- data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
- data/lib/uniword/wordprocessingml/settings.rb +8 -34
- data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/style.rb +2 -1
- data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
- data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
- data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
- data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
- data/lib/uniword/wordprocessingml.rb +4 -0
- data/lib/uniword.rb +27 -2
- metadata +38 -19
- data/config/validation_rules.yml +0 -60
- data/config/warning_rules.yml +0 -57
- data/lib/uniword/validation/document_validator.rb +0 -272
- data/lib/uniword/validation/structural_validator.rb +0 -116
- data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
- data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
- data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
- data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
- data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
- data/lib/uniword/validators/element_validator.rb +0 -93
- data/lib/uniword/validators/paragraph_validator.rb +0 -116
- data/lib/uniword/validators/table_validator.rb +0 -134
- data/lib/uniword/validators.rb +0 -9
- data/lib/uniword/warnings/warning.rb +0 -130
- data/lib/uniword/warnings/warning_collector.rb +0 -234
- data/lib/uniword/warnings/warning_report.rb +0 -159
- data/lib/uniword/warnings.rb +0 -9
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
# All Validation classes autoloaded via lib/uniword/validation.rb and validators.rb
|
|
5
|
-
# Configuration::ConfigurationLoader autoloaded via lib/uniword.rb
|
|
6
|
-
|
|
7
|
-
module Uniword
|
|
8
|
-
module Validation
|
|
9
|
-
# Main document validator orchestrator.
|
|
10
|
-
#
|
|
11
|
-
# Responsibility: Orchestrate 7-layer document validation.
|
|
12
|
-
# Single Responsibility: Only coordinates validation layers.
|
|
13
|
-
#
|
|
14
|
-
# Validates Word documents at 7 layers (MECE):
|
|
15
|
-
# 1. File Structure - file exists, readable, valid extension
|
|
16
|
-
# 2. ZIP Integrity - valid archive, no corruption
|
|
17
|
-
# 3. OOXML Parts - required parts present
|
|
18
|
-
# 4. XML Schema - well-formed XML
|
|
19
|
-
# 5. Relationships - valid references
|
|
20
|
-
# 6. Content Types - consistent type declarations
|
|
21
|
-
# 7. Document Semantics - valid document structure
|
|
22
|
-
#
|
|
23
|
-
# External configuration: config/validation_rules.yml
|
|
24
|
-
#
|
|
25
|
-
# @example Basic validation
|
|
26
|
-
# validator = DocumentValidator.new
|
|
27
|
-
# report = validator.validate('document.docx')
|
|
28
|
-
# puts report.valid? # => true
|
|
29
|
-
#
|
|
30
|
-
# @example Custom configuration
|
|
31
|
-
# validator = DocumentValidator.new(
|
|
32
|
-
# config_file: 'custom_validation.yml'
|
|
33
|
-
# )
|
|
34
|
-
# report = validator.validate('document.docx')
|
|
35
|
-
class DocumentValidator
|
|
36
|
-
# @return [Hash] Configuration
|
|
37
|
-
attr_reader :config
|
|
38
|
-
|
|
39
|
-
# @return [Array<LayerValidator>] Validators
|
|
40
|
-
attr_reader :validators
|
|
41
|
-
|
|
42
|
-
# Initialize document validator.
|
|
43
|
-
#
|
|
44
|
-
# @param config_file [String] Path to configuration file
|
|
45
|
-
#
|
|
46
|
-
# @example Create with default config
|
|
47
|
-
# validator = DocumentValidator.new
|
|
48
|
-
#
|
|
49
|
-
# @example Create with custom config
|
|
50
|
-
# validator = DocumentValidator.new(
|
|
51
|
-
# config_file: 'config/custom_validation.yml'
|
|
52
|
-
# )
|
|
53
|
-
def initialize(config_file: nil)
|
|
54
|
-
@config = load_config(config_file)
|
|
55
|
-
@validators = initialize_validators
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Validate a document file.
|
|
59
|
-
#
|
|
60
|
-
# Runs all enabled validators in sequence. Can fail-fast on
|
|
61
|
-
# critical errors if configured.
|
|
62
|
-
#
|
|
63
|
-
# @param path [String] Path to .docx file
|
|
64
|
-
# @return [DocumentValidationReport] Validation results
|
|
65
|
-
#
|
|
66
|
-
# @example Validate a document
|
|
67
|
-
# report = validator.validate('document.docx')
|
|
68
|
-
# if report.valid?
|
|
69
|
-
# puts "Document is valid!"
|
|
70
|
-
# else
|
|
71
|
-
# puts "Errors: #{report.errors.count}"
|
|
72
|
-
# end
|
|
73
|
-
def validate(path)
|
|
74
|
-
report = DocumentValidationReport.new(path)
|
|
75
|
-
|
|
76
|
-
@validators.each do |validator|
|
|
77
|
-
next unless validator.enabled?
|
|
78
|
-
|
|
79
|
-
result = validator.validate(path)
|
|
80
|
-
report.add_layer_result(validator.layer_name, result)
|
|
81
|
-
|
|
82
|
-
# Fail fast if critical failure and configured to do so
|
|
83
|
-
break if result.critical_failures? && fail_fast?
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
report
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Quick validity check (boolean).
|
|
90
|
-
#
|
|
91
|
-
# @param path [String] Path to .docx file
|
|
92
|
-
# @return [Boolean] true if document is valid
|
|
93
|
-
#
|
|
94
|
-
# @example Quick check
|
|
95
|
-
# if validator.valid?('document.docx')
|
|
96
|
-
# # Process document
|
|
97
|
-
# end
|
|
98
|
-
def valid?(path)
|
|
99
|
-
validate(path).valid?
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def load_config(config_file)
|
|
105
|
-
if config_file
|
|
106
|
-
Configuration::ConfigurationLoader.load_file(config_file)
|
|
107
|
-
else
|
|
108
|
-
# Try to load default config
|
|
109
|
-
default_path = File.join(
|
|
110
|
-
Configuration::ConfigurationLoader::CONFIG_DIR,
|
|
111
|
-
"validation_rules.yml",
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
if File.exist?(default_path)
|
|
115
|
-
Configuration::ConfigurationLoader.load("validation_rules")
|
|
116
|
-
else
|
|
117
|
-
# Use empty config with defaults
|
|
118
|
-
{ document_validation: { fail_fast: true } }
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
rescue Configuration::ConfigurationError
|
|
122
|
-
# If config fails to load, use defaults
|
|
123
|
-
{ document_validation: { fail_fast: true } }
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def initialize_validators
|
|
127
|
-
config_hash = @config[:document_validation] || @config
|
|
128
|
-
|
|
129
|
-
[
|
|
130
|
-
Validators::FileStructureValidator.new(config_hash),
|
|
131
|
-
Validators::ZipIntegrityValidator.new(config_hash),
|
|
132
|
-
Validators::OoxmlPartValidator.new(config_hash),
|
|
133
|
-
Validators::XmlSchemaValidator.new(config_hash),
|
|
134
|
-
Validators::RelationshipValidator.new(config_hash),
|
|
135
|
-
Validators::ContentTypeValidator.new(config_hash),
|
|
136
|
-
Validators::DocumentSemanticsValidator.new(config_hash),
|
|
137
|
-
]
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def fail_fast?
|
|
141
|
-
validation_config = @config[:document_validation] || {}
|
|
142
|
-
validation_config[:fail_fast] != false
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Validation report for document validation.
|
|
147
|
-
#
|
|
148
|
-
# Aggregates results from all validation layers.
|
|
149
|
-
class DocumentValidationReport
|
|
150
|
-
# @return [String] Path to validated file
|
|
151
|
-
attr_reader :file_path
|
|
152
|
-
|
|
153
|
-
# @return [Hash<String, LayerValidationResult>] Results by layer
|
|
154
|
-
attr_reader :layer_results
|
|
155
|
-
|
|
156
|
-
# Initialize a new report.
|
|
157
|
-
#
|
|
158
|
-
# @param file_path [String] Path to validated file
|
|
159
|
-
def initialize(file_path)
|
|
160
|
-
@file_path = file_path
|
|
161
|
-
@layer_results = {}
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Add a layer validation result.
|
|
165
|
-
#
|
|
166
|
-
# @param layer_name [String] Name of validation layer
|
|
167
|
-
# @param result [LayerValidationResult] Validation result
|
|
168
|
-
# @return [void]
|
|
169
|
-
def add_layer_result(layer_name, result)
|
|
170
|
-
@layer_results[layer_name] = result
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Check if document is valid overall.
|
|
174
|
-
#
|
|
175
|
-
# @return [Boolean] true if all layers passed
|
|
176
|
-
def valid?
|
|
177
|
-
@layer_results.values.all?(&:valid?)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Get all errors from all layers.
|
|
181
|
-
#
|
|
182
|
-
# @return [Array<Hash>] All errors with layer info
|
|
183
|
-
def errors
|
|
184
|
-
@layer_results.flat_map do |layer, result|
|
|
185
|
-
result.errors.map { |err| err.merge(layer: layer) }
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Get all warnings from all layers.
|
|
190
|
-
#
|
|
191
|
-
# @return [Array<Hash>] All warnings with layer info
|
|
192
|
-
def warnings
|
|
193
|
-
@layer_results.flat_map do |layer, result|
|
|
194
|
-
result.warnings.map { |warn| warn.merge(layer: layer) }
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Get all info messages from all layers.
|
|
199
|
-
#
|
|
200
|
-
# @return [Array<Hash>] All info messages with layer info
|
|
201
|
-
def infos
|
|
202
|
-
@layer_results.flat_map do |layer, result|
|
|
203
|
-
result.infos.map { |info| info.merge(layer: layer) }
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Check if any layer has critical failures.
|
|
208
|
-
#
|
|
209
|
-
# @return [Boolean] true if critical failures exist
|
|
210
|
-
def critical_failures?
|
|
211
|
-
@layer_results.values.any?(&:critical_failures?)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Get summary statistics.
|
|
215
|
-
#
|
|
216
|
-
# @return [Hash] Summary data
|
|
217
|
-
def summary
|
|
218
|
-
{
|
|
219
|
-
file: @file_path,
|
|
220
|
-
valid: valid?,
|
|
221
|
-
layers_validated: @layer_results.count,
|
|
222
|
-
errors: errors.count,
|
|
223
|
-
warnings: warnings.count,
|
|
224
|
-
infos: infos.count,
|
|
225
|
-
critical: critical_failures?,
|
|
226
|
-
}
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Export to JSON string.
|
|
230
|
-
#
|
|
231
|
-
# @return [String] JSON representation
|
|
232
|
-
def to_json(*_args)
|
|
233
|
-
JSON.pretty_generate(
|
|
234
|
-
summary: summary,
|
|
235
|
-
layers: @layer_results.transform_values(&:to_h),
|
|
236
|
-
)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Convert to string for display.
|
|
240
|
-
#
|
|
241
|
-
# @return [String] String representation
|
|
242
|
-
def to_s
|
|
243
|
-
lines = [
|
|
244
|
-
"Document Validation Report: #{@file_path}",
|
|
245
|
-
"=" * 60,
|
|
246
|
-
"",
|
|
247
|
-
]
|
|
248
|
-
|
|
249
|
-
@layer_results.each_value do |result|
|
|
250
|
-
lines << result.to_s
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
lines << ""
|
|
254
|
-
lines << "Summary:"
|
|
255
|
-
lines << " Valid: #{valid?}"
|
|
256
|
-
lines << " Errors: #{errors.count}"
|
|
257
|
-
lines << " Warnings: #{warnings.count}"
|
|
258
|
-
lines << " Info: #{infos.count}"
|
|
259
|
-
|
|
260
|
-
if errors.any?
|
|
261
|
-
lines << ""
|
|
262
|
-
lines << "Errors:"
|
|
263
|
-
errors.each do |error|
|
|
264
|
-
lines << " [#{error[:layer]}] #{error[:message]}"
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
lines.join("\n")
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
end
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Uniword
|
|
4
|
-
module Validation
|
|
5
|
-
# Validates in-memory DocumentRoot structure.
|
|
6
|
-
#
|
|
7
|
-
# Performs structural checks on the document model without requiring
|
|
8
|
-
# file access. Complements DocumentValidator (file-level 7-layer
|
|
9
|
-
# pipeline) for quick in-process validation.
|
|
10
|
-
#
|
|
11
|
-
# Follows Open/Closed: add new check methods and register them in
|
|
12
|
-
# CHECKS without modifying existing code.
|
|
13
|
-
#
|
|
14
|
-
# @example
|
|
15
|
-
# validator = StructuralValidator.new(document)
|
|
16
|
-
# validator.valid? # => true/false
|
|
17
|
-
# validator.errors # => ["bookmarkStart id='42' has no bookmarkEnd"]
|
|
18
|
-
# validator.warnings # => ["Empty paragraph at index 7"]
|
|
19
|
-
class StructuralValidator
|
|
20
|
-
# Registered check methods. Each returns an array of
|
|
21
|
-
# { severity:, message: } hashes.
|
|
22
|
-
CHECKS = %i[
|
|
23
|
-
check_body_present
|
|
24
|
-
check_bookmark_pairing
|
|
25
|
-
check_bookmark_uniqueness
|
|
26
|
-
check_empty_paragraphs
|
|
27
|
-
].freeze
|
|
28
|
-
|
|
29
|
-
attr_reader :document
|
|
30
|
-
|
|
31
|
-
def initialize(document)
|
|
32
|
-
@document = document
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# @return [Boolean] true if no errors (warnings are non-fatal)
|
|
36
|
-
def valid?
|
|
37
|
-
errors.empty?
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# @return [Array<String>] Error messages (severity: error)
|
|
41
|
-
def errors
|
|
42
|
-
@errors ||= issues
|
|
43
|
-
.select { |i| i[:severity] == :error }
|
|
44
|
-
.map { |i| i[:message] }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# @return [Array<String>] Warning messages (severity: warning)
|
|
48
|
-
def warnings
|
|
49
|
-
@warnings ||= issues
|
|
50
|
-
.select { |i| i[:severity] == :warning }
|
|
51
|
-
.map { |i| i[:message] }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# @return [Array<Hash>] All issues
|
|
55
|
-
def issues
|
|
56
|
-
@issues ||= CHECKS.flat_map { |check| send(check) }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
def check_body_present
|
|
62
|
-
return [] if document.body
|
|
63
|
-
|
|
64
|
-
[{ severity: :error,
|
|
65
|
-
message: "Document body is missing" }]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def check_bookmark_pairing
|
|
69
|
-
return [] unless document.body
|
|
70
|
-
|
|
71
|
-
starts = document.body.bookmark_starts || []
|
|
72
|
-
ends = document.body.bookmark_ends || []
|
|
73
|
-
end_ids = ends.filter_map(&:id).to_set
|
|
74
|
-
|
|
75
|
-
starts.filter_map do |bs|
|
|
76
|
-
next if end_ids.include?(bs.id)
|
|
77
|
-
|
|
78
|
-
{ severity: :error,
|
|
79
|
-
message: "bookmarkStart id='#{bs.id}' (name='#{bs.name}') " \
|
|
80
|
-
"has no matching bookmarkEnd" }
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def check_bookmark_uniqueness
|
|
85
|
-
return [] unless document.body
|
|
86
|
-
|
|
87
|
-
starts = document.body.bookmark_starts || []
|
|
88
|
-
seen = {}
|
|
89
|
-
starts.filter_map do |bs|
|
|
90
|
-
next unless bs.name
|
|
91
|
-
next if bs.name.to_s == "_GoBack" # Word internal bookmark
|
|
92
|
-
|
|
93
|
-
if seen[bs.name.to_s]
|
|
94
|
-
{ severity: :warning,
|
|
95
|
-
message: "Duplicate bookmark name '#{bs.name}'" }
|
|
96
|
-
else
|
|
97
|
-
seen[bs.name.to_s] = true
|
|
98
|
-
nil
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def check_empty_paragraphs
|
|
104
|
-
return [] unless document.body
|
|
105
|
-
|
|
106
|
-
paragraphs = document.body.paragraphs || []
|
|
107
|
-
paragraphs.each_with_index.filter_map do |para, idx|
|
|
108
|
-
next unless para.runs.nil? || para.runs.empty?
|
|
109
|
-
|
|
110
|
-
{ severity: :warning,
|
|
111
|
-
message: "Empty paragraph at index #{idx}" }
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "zip"
|
|
4
|
-
require "nokogiri"
|
|
5
|
-
# LayerValidator autoloaded via lib/uniword/validation.rb
|
|
6
|
-
|
|
7
|
-
module Uniword
|
|
8
|
-
module Validation
|
|
9
|
-
module Validators
|
|
10
|
-
# Validates content type consistency.
|
|
11
|
-
#
|
|
12
|
-
# Responsibility: Validate [Content_Types].xml declarations.
|
|
13
|
-
# Single Responsibility: Only validates content type definitions.
|
|
14
|
-
#
|
|
15
|
-
# This is Layer 6 validation - validates that content types are
|
|
16
|
-
# properly declared and consistent with actual file extensions.
|
|
17
|
-
#
|
|
18
|
-
# Checks:
|
|
19
|
-
# - [Content_Types].xml is well-formed
|
|
20
|
-
# - Extensions and overrides are consistent
|
|
21
|
-
# - Required content types are declared
|
|
22
|
-
#
|
|
23
|
-
# @example Validate content types
|
|
24
|
-
# validator = ContentTypeValidator.new(config)
|
|
25
|
-
# result = validator.validate('/path/to/document.docx')
|
|
26
|
-
# puts result.valid? # => true
|
|
27
|
-
class ContentTypeValidator < LayerValidator
|
|
28
|
-
CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types"
|
|
29
|
-
|
|
30
|
-
REQUIRED_CONTENT_TYPES = [
|
|
31
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
|
|
32
|
-
"application/vnd.openxmlformats-package.relationships+xml",
|
|
33
|
-
].freeze
|
|
34
|
-
|
|
35
|
-
def layer_name
|
|
36
|
-
"Content Types"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def validate(path)
|
|
40
|
-
result = LayerValidationResult.new(layer_name)
|
|
41
|
-
|
|
42
|
-
Zip::File.open(path) do |zip|
|
|
43
|
-
validate_content_types(zip, result)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
result
|
|
47
|
-
rescue Zip::Error => e
|
|
48
|
-
result.add_error(
|
|
49
|
-
"Cannot open ZIP file: #{e.message}",
|
|
50
|
-
critical: true,
|
|
51
|
-
)
|
|
52
|
-
result
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def validate_content_types(zip, result)
|
|
58
|
-
entry = zip.find_entry("[Content_Types].xml")
|
|
59
|
-
|
|
60
|
-
unless entry
|
|
61
|
-
result.add_error(
|
|
62
|
-
"Missing [Content_Types].xml",
|
|
63
|
-
critical: true,
|
|
64
|
-
)
|
|
65
|
-
return
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
validate_content_types_file(zip, entry, result)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def validate_content_types_file(zip, entry, result)
|
|
72
|
-
xml_content = entry.get_input_stream.read
|
|
73
|
-
doc = Nokogiri::XML(xml_content)
|
|
74
|
-
|
|
75
|
-
# Check for required content types
|
|
76
|
-
validate_required_types(doc, result) if check_consistency?
|
|
77
|
-
|
|
78
|
-
# Validate extensions match actual files
|
|
79
|
-
if validate_extensions?
|
|
80
|
-
validate_extension_consistency(zip, doc,
|
|
81
|
-
result)
|
|
82
|
-
end
|
|
83
|
-
rescue Nokogiri::XML::SyntaxError => e
|
|
84
|
-
result.add_error(
|
|
85
|
-
"Malformed [Content_Types].xml: #{e.message}",
|
|
86
|
-
critical: true,
|
|
87
|
-
)
|
|
88
|
-
rescue StandardError => e
|
|
89
|
-
result.add_error(
|
|
90
|
-
"Failed to validate content types: #{e.message}",
|
|
91
|
-
)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def validate_required_types(doc, result)
|
|
95
|
-
declared_types = extract_declared_types(doc)
|
|
96
|
-
|
|
97
|
-
REQUIRED_CONTENT_TYPES.each do |required_type|
|
|
98
|
-
next if declared_types.include?(required_type)
|
|
99
|
-
|
|
100
|
-
result.add_warning(
|
|
101
|
-
"Required content type not declared: #{required_type}",
|
|
102
|
-
)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def extract_declared_types(doc)
|
|
107
|
-
types = []
|
|
108
|
-
|
|
109
|
-
# From Default elements
|
|
110
|
-
doc.xpath("//xmlns:Default",
|
|
111
|
-
"xmlns" => CONTENT_TYPES_NAMESPACE).each do |default|
|
|
112
|
-
types << default["ContentType"] if default["ContentType"]
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# From Override elements
|
|
116
|
-
doc.xpath("//xmlns:Override",
|
|
117
|
-
"xmlns" => CONTENT_TYPES_NAMESPACE).each do |override|
|
|
118
|
-
types << override["ContentType"] if override["ContentType"]
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
types
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def validate_extension_consistency(zip, doc, result)
|
|
125
|
-
# Get declared extensions
|
|
126
|
-
extensions = {}
|
|
127
|
-
doc.xpath("//xmlns:Default",
|
|
128
|
-
"xmlns" => CONTENT_TYPES_NAMESPACE).each do |default|
|
|
129
|
-
ext = default["Extension"]
|
|
130
|
-
content_type = default["ContentType"]
|
|
131
|
-
extensions[ext] = content_type if ext && content_type
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Check if files with declared extensions exist
|
|
135
|
-
zip.entries.each do |entry|
|
|
136
|
-
next if entry.directory?
|
|
137
|
-
|
|
138
|
-
ext = File.extname(entry.name)[1..] # Remove leading dot
|
|
139
|
-
next unless ext
|
|
140
|
-
|
|
141
|
-
if extensions.key?(ext)
|
|
142
|
-
# Expected extension found
|
|
143
|
-
else
|
|
144
|
-
result.add_info(
|
|
145
|
-
"File #{entry.name} has undeclared extension: .#{ext}",
|
|
146
|
-
)
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def check_consistency?
|
|
152
|
-
layer_config[:check_consistency] != false
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def validate_extensions?
|
|
156
|
-
layer_config[:validate_extensions] != false
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def layer_config
|
|
160
|
-
@config[:content_types] || {}
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# LayerValidator autoloaded via lib/uniword/validation.rb
|
|
4
|
-
|
|
5
|
-
module Uniword
|
|
6
|
-
module Validation
|
|
7
|
-
module Validators
|
|
8
|
-
# Validates file structure (existence, readability, extension, size).
|
|
9
|
-
#
|
|
10
|
-
# Responsibility: Validate file-level properties.
|
|
11
|
-
# Single Responsibility: Only validates file structure.
|
|
12
|
-
#
|
|
13
|
-
# This is Layer 1 validation - the most basic check that must pass
|
|
14
|
-
# before any other validation can occur.
|
|
15
|
-
#
|
|
16
|
-
# Checks:
|
|
17
|
-
# - File exists
|
|
18
|
-
# - File is readable
|
|
19
|
-
# - File extension is appropriate
|
|
20
|
-
# - File size is reasonable
|
|
21
|
-
#
|
|
22
|
-
# @example Validate a file
|
|
23
|
-
# validator = FileStructureValidator.new(config)
|
|
24
|
-
# result = validator.validate('/path/to/document.docx')
|
|
25
|
-
# puts result.valid? # => true
|
|
26
|
-
class FileStructureValidator < LayerValidator
|
|
27
|
-
def layer_name
|
|
28
|
-
"File Structure"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def validate(path)
|
|
32
|
-
result = LayerValidationResult.new(layer_name)
|
|
33
|
-
|
|
34
|
-
# Check file exists
|
|
35
|
-
unless File.exist?(path)
|
|
36
|
-
return result.add_error("File does not exist",
|
|
37
|
-
critical: true)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Check readable
|
|
41
|
-
unless File.readable?(path)
|
|
42
|
-
return result.add_error("File is not readable",
|
|
43
|
-
critical: true)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Check extension
|
|
47
|
-
validate_extension(path, result) if check_extension?
|
|
48
|
-
|
|
49
|
-
# Check file size
|
|
50
|
-
validate_size(path, result) if check_size?
|
|
51
|
-
|
|
52
|
-
result
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def check_extension?
|
|
58
|
-
layer_config[:check_extension] != false
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def check_size?
|
|
62
|
-
layer_config[:check_size] != false
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def layer_config
|
|
66
|
-
@config[:file_structure] || {}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def validate_extension(path, result)
|
|
70
|
-
allowed = layer_config[:allowed_extensions] || [".docx", ".doc"]
|
|
71
|
-
|
|
72
|
-
return if allowed.any? { |ext| path.end_with?(ext) }
|
|
73
|
-
|
|
74
|
-
result.add_warning(
|
|
75
|
-
"Unusual file extension (expected #{allowed.join(' or ')})",
|
|
76
|
-
)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def validate_size(path, result)
|
|
80
|
-
size = File.size(path)
|
|
81
|
-
max_size = @config[:max_file_size] || 104_857_600 # 100MB default
|
|
82
|
-
|
|
83
|
-
if size.zero?
|
|
84
|
-
result.add_error("File is empty", critical: true)
|
|
85
|
-
elsif size > max_size
|
|
86
|
-
result.add_warning(
|
|
87
|
-
"File is very large (#{format_bytes(size)})",
|
|
88
|
-
)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def format_bytes(bytes)
|
|
93
|
-
if bytes < 1024
|
|
94
|
-
"#{bytes} bytes"
|
|
95
|
-
elsif bytes < 1024 * 1024
|
|
96
|
-
"#{(bytes / 1024.0).round(1)} KB"
|
|
97
|
-
else
|
|
98
|
-
"#{(bytes / (1024.0 * 1024)).round(1)} MB"
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|