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,128 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "zip"
|
|
4
|
-
# LayerValidator autoloaded via lib/uniword/validation.rb
|
|
5
|
-
|
|
6
|
-
module Uniword
|
|
7
|
-
module Validation
|
|
8
|
-
module Validators
|
|
9
|
-
# Validates OOXML part structure (required/optional parts).
|
|
10
|
-
#
|
|
11
|
-
# Responsibility: Validate OOXML parts exist per ISO/IEC 29500.
|
|
12
|
-
# Single Responsibility: Only validates part existence.
|
|
13
|
-
#
|
|
14
|
-
# This is Layer 3 validation - validates that all required OOXML
|
|
15
|
-
# parts are present in the document package.
|
|
16
|
-
#
|
|
17
|
-
# Checks:
|
|
18
|
-
# - Required parts exist (Content Types, document, relationships)
|
|
19
|
-
# - Optional but common parts (styles, numbering)
|
|
20
|
-
#
|
|
21
|
-
# @example Validate OOXML parts
|
|
22
|
-
# validator = OoxmlPartValidator.new(config)
|
|
23
|
-
# result = validator.validate('/path/to/document.docx')
|
|
24
|
-
# puts result.valid? # => true
|
|
25
|
-
class OoxmlPartValidator < LayerValidator
|
|
26
|
-
# Required parts per ISO/IEC 29500
|
|
27
|
-
REQUIRED_PARTS = {
|
|
28
|
-
"[Content_Types].xml" => "Content Types definition",
|
|
29
|
-
"word/document.xml" => "Main document content",
|
|
30
|
-
"_rels/.rels" => "Package relationships",
|
|
31
|
-
}.freeze
|
|
32
|
-
|
|
33
|
-
# Optional but commonly used parts
|
|
34
|
-
OPTIONAL_PARTS = {
|
|
35
|
-
"word/styles.xml" => "Styles definition",
|
|
36
|
-
"word/numbering.xml" => "Numbering definition",
|
|
37
|
-
"word/_rels/document.xml.rels" => "Document relationships",
|
|
38
|
-
"word/settings.xml" => "Document settings",
|
|
39
|
-
"word/fontTable.xml" => "Font table",
|
|
40
|
-
"word/theme/theme1.xml" => "Theme definition",
|
|
41
|
-
}.freeze
|
|
42
|
-
|
|
43
|
-
def layer_name
|
|
44
|
-
"OOXML Parts"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def validate(path)
|
|
48
|
-
result = LayerValidationResult.new(layer_name)
|
|
49
|
-
|
|
50
|
-
Zip::File.open(path) do |zip|
|
|
51
|
-
validate_required_parts(zip, result)
|
|
52
|
-
validate_optional_parts(zip, result) if warn_missing_optional?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
result
|
|
56
|
-
rescue Zip::Error => e
|
|
57
|
-
result.add_error(
|
|
58
|
-
"Cannot open ZIP file: #{e.message}",
|
|
59
|
-
critical: true,
|
|
60
|
-
)
|
|
61
|
-
result
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def validate_required_parts(zip, result)
|
|
67
|
-
return unless require_content_types? ||
|
|
68
|
-
require_document? ||
|
|
69
|
-
require_relationships?
|
|
70
|
-
|
|
71
|
-
REQUIRED_PARTS.each do |part_name, description|
|
|
72
|
-
next unless should_check_part?(part_name)
|
|
73
|
-
|
|
74
|
-
next if zip.find_entry(part_name)
|
|
75
|
-
|
|
76
|
-
result.add_error(
|
|
77
|
-
"Missing #{description}: #{part_name}",
|
|
78
|
-
critical: true,
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def validate_optional_parts(zip, result)
|
|
84
|
-
OPTIONAL_PARTS.each do |part_name, description|
|
|
85
|
-
next if zip.find_entry(part_name)
|
|
86
|
-
|
|
87
|
-
result.add_info(
|
|
88
|
-
"Missing #{description}: #{part_name} (optional)",
|
|
89
|
-
)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def should_check_part?(part_name)
|
|
94
|
-
case part_name
|
|
95
|
-
when "[Content_Types].xml"
|
|
96
|
-
require_content_types?
|
|
97
|
-
when "word/document.xml"
|
|
98
|
-
require_document?
|
|
99
|
-
when "_rels/.rels"
|
|
100
|
-
require_relationships?
|
|
101
|
-
else
|
|
102
|
-
true
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def require_content_types?
|
|
107
|
-
layer_config[:require_content_types] != false
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def require_document?
|
|
111
|
-
layer_config[:require_document] != false
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def require_relationships?
|
|
115
|
-
layer_config[:require_relationships] != false
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def warn_missing_optional?
|
|
119
|
-
layer_config[:warn_missing_optional] != false
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def layer_config
|
|
123
|
-
@config[:ooxml_parts] || {}
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
@@ -1,147 +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 relationship integrity (targets exist, no broken refs).
|
|
11
|
-
#
|
|
12
|
-
# Responsibility: Validate relationship files and targets.
|
|
13
|
-
# Single Responsibility: Only validates relationships.
|
|
14
|
-
#
|
|
15
|
-
# This is Layer 5 validation - validates that all relationship
|
|
16
|
-
# files are valid and their targets exist.
|
|
17
|
-
#
|
|
18
|
-
# Checks:
|
|
19
|
-
# - Relationship files are well-formed
|
|
20
|
-
# - Relationship targets exist
|
|
21
|
-
# - Relationship types are valid
|
|
22
|
-
#
|
|
23
|
-
# @example Validate relationships
|
|
24
|
-
# validator = RelationshipValidator.new(config)
|
|
25
|
-
# result = validator.validate('/path/to/document.docx')
|
|
26
|
-
# puts result.valid? # => true
|
|
27
|
-
class RelationshipValidator < LayerValidator
|
|
28
|
-
RELATIONSHIP_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships"
|
|
29
|
-
|
|
30
|
-
def layer_name
|
|
31
|
-
"Relationships"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def validate(path)
|
|
35
|
-
result = LayerValidationResult.new(layer_name)
|
|
36
|
-
|
|
37
|
-
Zip::File.open(path) do |zip|
|
|
38
|
-
validate_relationships(zip, result)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
result
|
|
42
|
-
rescue Zip::Error => e
|
|
43
|
-
result.add_error(
|
|
44
|
-
"Cannot open ZIP file: #{e.message}",
|
|
45
|
-
critical: true,
|
|
46
|
-
)
|
|
47
|
-
result
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def validate_relationships(zip, result)
|
|
53
|
-
rel_entries = zip.entries.select { |e| e.name.end_with?(".rels") }
|
|
54
|
-
|
|
55
|
-
if rel_entries.empty?
|
|
56
|
-
result.add_warning("No relationship files found")
|
|
57
|
-
return
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
rel_entries.each do |entry|
|
|
61
|
-
validate_relationship_file(zip, entry, result)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def validate_relationship_file(zip, entry, result)
|
|
66
|
-
xml_content = entry.get_input_stream.read
|
|
67
|
-
doc = Nokogiri::XML(xml_content)
|
|
68
|
-
|
|
69
|
-
# Get the base directory for this .rels file
|
|
70
|
-
base_dir = File.dirname(entry.name.sub("/_rels/", "/"))
|
|
71
|
-
base_dir = "" if base_dir == "."
|
|
72
|
-
|
|
73
|
-
doc.xpath("//xmlns:Relationship",
|
|
74
|
-
"xmlns" => RELATIONSHIP_NAMESPACE).each do |rel|
|
|
75
|
-
validate_relationship(zip, rel, base_dir, result)
|
|
76
|
-
end
|
|
77
|
-
rescue Nokogiri::XML::SyntaxError => e
|
|
78
|
-
result.add_error(
|
|
79
|
-
"Malformed relationship file #{entry.name}: #{e.message}",
|
|
80
|
-
)
|
|
81
|
-
rescue StandardError => e
|
|
82
|
-
result.add_error(
|
|
83
|
-
"Failed to validate #{entry.name}: #{e.message}",
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def validate_relationship(zip, rel_node, base_dir, result)
|
|
88
|
-
target = rel_node["Target"]
|
|
89
|
-
type = rel_node["Type"]
|
|
90
|
-
target_mode = rel_node["TargetMode"]
|
|
91
|
-
|
|
92
|
-
# Skip external relationships
|
|
93
|
-
return if target_mode == "External"
|
|
94
|
-
|
|
95
|
-
# Check if target exists
|
|
96
|
-
if check_targets_exist? && target && !target.start_with?("#")
|
|
97
|
-
target_path = resolve_target_path(base_dir, target)
|
|
98
|
-
|
|
99
|
-
unless zip.find_entry(target_path)
|
|
100
|
-
result.add_error(
|
|
101
|
-
"Relationship target not found: #{target_path}",
|
|
102
|
-
)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Validate relationship type
|
|
107
|
-
return unless validate_types? && type
|
|
108
|
-
|
|
109
|
-
validate_relationship_type(type, result)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def resolve_target_path(base_dir, target)
|
|
113
|
-
# Handle absolute paths (starting with /)
|
|
114
|
-
return target[1..] if target.start_with?("/")
|
|
115
|
-
|
|
116
|
-
# Handle relative paths
|
|
117
|
-
if base_dir.empty?
|
|
118
|
-
target
|
|
119
|
-
else
|
|
120
|
-
File.join(base_dir, target)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def validate_relationship_type(type, result)
|
|
125
|
-
# Just validate it's a URI-like string
|
|
126
|
-
return if type.include?("/")
|
|
127
|
-
|
|
128
|
-
result.add_warning(
|
|
129
|
-
"Unusual relationship type: #{type}",
|
|
130
|
-
)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def check_targets_exist?
|
|
134
|
-
layer_config[:check_targets_exist] != false
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def validate_types?
|
|
138
|
-
layer_config[:validate_types] != false
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def layer_config
|
|
142
|
-
@config[:relationships] || {}
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "zip"
|
|
4
|
-
# LayerValidator autoloaded via lib/uniword/validation.rb
|
|
5
|
-
|
|
6
|
-
module Uniword
|
|
7
|
-
module Validation
|
|
8
|
-
module Validators
|
|
9
|
-
# Validates ZIP archive integrity.
|
|
10
|
-
#
|
|
11
|
-
# Responsibility: Validate ZIP structure and contents.
|
|
12
|
-
# Single Responsibility: Only validates ZIP integrity.
|
|
13
|
-
#
|
|
14
|
-
# This is Layer 2 validation - validates the ZIP container that
|
|
15
|
-
# holds all OOXML parts.
|
|
16
|
-
#
|
|
17
|
-
# Checks:
|
|
18
|
-
# - File opens as valid ZIP
|
|
19
|
-
# - ZIP is not empty
|
|
20
|
-
# - Required entries exist
|
|
21
|
-
# - Entries are not corrupted
|
|
22
|
-
#
|
|
23
|
-
# @example Validate ZIP integrity
|
|
24
|
-
# validator = ZipIntegrityValidator.new(config)
|
|
25
|
-
# result = validator.validate('/path/to/document.docx')
|
|
26
|
-
# puts result.valid? # => true
|
|
27
|
-
class ZipIntegrityValidator < LayerValidator
|
|
28
|
-
def layer_name
|
|
29
|
-
"ZIP Integrity"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def validate(path)
|
|
33
|
-
result = LayerValidationResult.new(layer_name)
|
|
34
|
-
|
|
35
|
-
begin
|
|
36
|
-
validate_zip_structure(path, result)
|
|
37
|
-
rescue Zip::Error => e
|
|
38
|
-
result.add_error(
|
|
39
|
-
"Invalid ZIP file: #{e.message}",
|
|
40
|
-
critical: true,
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
result
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def validate_zip_structure(path, result)
|
|
50
|
-
Zip::File.open(path) do |zip_file|
|
|
51
|
-
# Check ZIP is not empty
|
|
52
|
-
if zip_file.entries.empty?
|
|
53
|
-
result.add_error("ZIP archive is empty", critical: true)
|
|
54
|
-
return
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Check required entries
|
|
58
|
-
validate_required_entries(zip_file, result) if check_entries?
|
|
59
|
-
|
|
60
|
-
# Check for corrupted entries
|
|
61
|
-
validate_entry_integrity(zip_file, result) if check_corruption?
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def validate_required_entries(zip_file, result)
|
|
66
|
-
required_entries = [
|
|
67
|
-
"[Content_Types].xml",
|
|
68
|
-
"word/document.xml",
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
required_entries.each do |entry_name|
|
|
72
|
-
next if zip_file.find_entry(entry_name)
|
|
73
|
-
|
|
74
|
-
result.add_error(
|
|
75
|
-
"Missing required file: #{entry_name}",
|
|
76
|
-
critical: true,
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def validate_entry_integrity(zip_file, result)
|
|
82
|
-
zip_file.entries.each do |entry|
|
|
83
|
-
next if entry.directory?
|
|
84
|
-
|
|
85
|
-
begin
|
|
86
|
-
# Try to read first byte to check for corruption
|
|
87
|
-
entry.get_input_stream { |io| io.read(1) }
|
|
88
|
-
rescue StandardError => e
|
|
89
|
-
result.add_error(
|
|
90
|
-
"Corrupted entry: #{entry.name} (#{e.message})",
|
|
91
|
-
)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def check_entries?
|
|
97
|
-
layer_config[:check_entries] != false
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def check_corruption?
|
|
101
|
-
layer_config[:check_corruption] != false
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def layer_config
|
|
105
|
-
@config[:zip_integrity] || {}
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Uniword
|
|
4
|
-
module Validators
|
|
5
|
-
# Base validator for element validation
|
|
6
|
-
# Responsibility: Provide validation interface and registry for element validators
|
|
7
|
-
#
|
|
8
|
-
# This class follows the Single Responsibility Principle by separating
|
|
9
|
-
# validation logic from the element classes themselves.
|
|
10
|
-
#
|
|
11
|
-
# @example Using the validator
|
|
12
|
-
# validator = Uniword::Validators::ElementValidator.for(Paragraph)
|
|
13
|
-
# validator.valid?(paragraph) # => true or false
|
|
14
|
-
#
|
|
15
|
-
# @example Registering a custom validator
|
|
16
|
-
# Uniword::Validators::ElementValidator.register(MyElement, MyValidator)
|
|
17
|
-
class ElementValidator
|
|
18
|
-
class << self
|
|
19
|
-
# Get the appropriate validator for an element class
|
|
20
|
-
#
|
|
21
|
-
# @param element_class [Class] The element class to get validator for
|
|
22
|
-
# @return [ElementValidator] An instance of the appropriate validator
|
|
23
|
-
# @raise [ArgumentError] if element_class is not a valid Element class
|
|
24
|
-
def for(element_class)
|
|
25
|
-
unless element_class.is_a?(Class) &&
|
|
26
|
-
element_class.ancestors.any? do |a|
|
|
27
|
-
a.to_s.include?("Serializable")
|
|
28
|
-
end
|
|
29
|
-
raise ArgumentError,
|
|
30
|
-
"element_class must be a lutaml-model serializable class"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
validator_class = validator_registry[element_class] || self
|
|
34
|
-
validator_class.new
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Register a validator for a specific element class
|
|
38
|
-
#
|
|
39
|
-
# @param element_class [Class] The element class
|
|
40
|
-
# @param validator_class [Class] The validator class
|
|
41
|
-
# @return [void]
|
|
42
|
-
def register(element_class, validator_class)
|
|
43
|
-
unless validator_class.ancestors.include?(ElementValidator)
|
|
44
|
-
raise ArgumentError,
|
|
45
|
-
"validator_class must inherit from ElementValidator"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
validator_registry[element_class] = validator_class
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Get the validator registry
|
|
52
|
-
#
|
|
53
|
-
# @return [Hash] The registry mapping element classes to validators
|
|
54
|
-
def validator_registry
|
|
55
|
-
@validator_registry ||= {}
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Reset the validator registry (useful for testing)
|
|
59
|
-
#
|
|
60
|
-
# @return [void]
|
|
61
|
-
def reset_registry
|
|
62
|
-
@validator_registry = {}
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Validate an element
|
|
67
|
-
#
|
|
68
|
-
# @param element [Element] The element to validate
|
|
69
|
-
# @return [Boolean] true if valid, false otherwise
|
|
70
|
-
def valid?(element)
|
|
71
|
-
return false if element.nil?
|
|
72
|
-
# v2.0: Check if element is a serializable object (has lutaml-model ancestry)
|
|
73
|
-
return false unless element.class.ancestors.any? do |a|
|
|
74
|
-
a.to_s.include?("Serializable")
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
true
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Get validation errors for an element
|
|
81
|
-
#
|
|
82
|
-
# @param element [Element] The element to validate
|
|
83
|
-
# @return [Array<String>] Array of error messages
|
|
84
|
-
def errors(element)
|
|
85
|
-
return ["Element is nil"] if element.nil?
|
|
86
|
-
return ["Element must be a Uniword::Element"] unless element.class.ancestors.any? { |a| a.to_s.include?("Serializable") }
|
|
87
|
-
return [] if valid?(element)
|
|
88
|
-
|
|
89
|
-
["Element validation failed"]
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
@@ -1,116 +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 Paragraph elements
|
|
9
|
-
# Responsibility: Validate paragraph-specific constraints
|
|
10
|
-
#
|
|
11
|
-
# A valid paragraph:
|
|
12
|
-
# - Must be a Paragraph instance
|
|
13
|
-
# - Can have zero or more runs (empty paragraphs are valid)
|
|
14
|
-
# - All runs must be valid Run instances
|
|
15
|
-
# - Properties, if present, must be valid ParagraphProperties
|
|
16
|
-
#
|
|
17
|
-
# @example Using the paragraph validator
|
|
18
|
-
# validator = Uniword::Validators::ParagraphValidator.new
|
|
19
|
-
# validator.valid?(paragraph) # => true or false
|
|
20
|
-
# validator.errors(paragraph) # => ["error message", ...]
|
|
21
|
-
class ParagraphValidator < ElementValidator
|
|
22
|
-
# Validate a paragraph element
|
|
23
|
-
#
|
|
24
|
-
# @param element [Paragraph] The paragraph to validate
|
|
25
|
-
# @return [Boolean] true if valid, false otherwise
|
|
26
|
-
def valid?(element)
|
|
27
|
-
return false unless super
|
|
28
|
-
return false unless element.is_a?(Uniword::Wordprocessingml::Paragraph)
|
|
29
|
-
|
|
30
|
-
validate_runs(element) &&
|
|
31
|
-
validate_properties(element)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Get validation errors for a paragraph
|
|
35
|
-
#
|
|
36
|
-
# @param element [Paragraph] The paragraph to validate
|
|
37
|
-
# @return [Array<String>] Array of error messages
|
|
38
|
-
def errors(element)
|
|
39
|
-
errors = []
|
|
40
|
-
|
|
41
|
-
# Check if element is nil
|
|
42
|
-
return ["Element is nil"] if element.nil?
|
|
43
|
-
|
|
44
|
-
# Check paragraph type first (more specific than base check)
|
|
45
|
-
return ["Element must be a Paragraph"] unless element.is_a?(Uniword::Wordprocessingml::Paragraph)
|
|
46
|
-
|
|
47
|
-
# Validate runs - collect all specific errors
|
|
48
|
-
errors.concat(run_errors(element))
|
|
49
|
-
|
|
50
|
-
# Validate properties - collect all specific errors
|
|
51
|
-
errors.concat(property_errors(element))
|
|
52
|
-
|
|
53
|
-
errors
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
# Validate that all runs are valid Run instances
|
|
59
|
-
#
|
|
60
|
-
# @param paragraph [Paragraph] The paragraph to validate
|
|
61
|
-
# @return [Boolean] true if all runs are valid
|
|
62
|
-
def validate_runs(paragraph)
|
|
63
|
-
return true if paragraph.runs.nil? || paragraph.runs.empty?
|
|
64
|
-
|
|
65
|
-
paragraph.runs.all?(Uniword::Wordprocessingml::Run)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Get errors related to runs
|
|
69
|
-
#
|
|
70
|
-
# @param paragraph [Paragraph] The paragraph to validate
|
|
71
|
-
# @return [Array<String>] Array of error messages
|
|
72
|
-
def run_errors(paragraph)
|
|
73
|
-
errors = []
|
|
74
|
-
|
|
75
|
-
return errors if paragraph.runs.nil? || paragraph.runs.empty?
|
|
76
|
-
|
|
77
|
-
paragraph.runs.each_with_index do |run, index|
|
|
78
|
-
errors << "Run at index #{index} must be a Run instance" unless run.is_a?(Uniword::Wordprocessingml::Run)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
errors
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Validate properties if present
|
|
85
|
-
#
|
|
86
|
-
# @param paragraph [Paragraph] The paragraph to validate
|
|
87
|
-
# @return [Boolean] true if properties are valid or nil
|
|
88
|
-
def validate_properties(paragraph)
|
|
89
|
-
return true if paragraph.properties.nil?
|
|
90
|
-
|
|
91
|
-
paragraph.properties.is_a?(Uniword::Wordprocessingml::ParagraphProperties)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Get errors related to properties
|
|
95
|
-
#
|
|
96
|
-
# @param paragraph [Paragraph] The paragraph to validate
|
|
97
|
-
# @return [Array<String>] Array of error messages
|
|
98
|
-
def property_errors(paragraph)
|
|
99
|
-
return [] if paragraph.properties.nil?
|
|
100
|
-
|
|
101
|
-
return ["Properties must be a ParagraphProperties instance"] unless paragraph.properties.is_a?(Uniword::Wordprocessingml::ParagraphProperties)
|
|
102
|
-
|
|
103
|
-
[]
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Register this validator when the file is loaded
|
|
110
|
-
# This ensures the validator is available regardless of load order
|
|
111
|
-
if defined?(Uniword::Validators::ElementValidator)
|
|
112
|
-
Uniword::Validators::ElementValidator.register(
|
|
113
|
-
Uniword::Wordprocessingml::Paragraph,
|
|
114
|
-
Uniword::Validators::ParagraphValidator,
|
|
115
|
-
)
|
|
116
|
-
end
|