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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Uniword
|
|
7
|
+
module Docx
|
|
8
|
+
# Write-time OPC package integrity gate.
|
|
9
|
+
#
|
|
10
|
+
# Pure, non-mutating checker invoked on the in-memory ZIP content hash
|
|
11
|
+
# after reconciliation and relationship injection, before ZipPackager.
|
|
12
|
+
# MECE with the mutating Reconciler: the Reconciler repairs what it can,
|
|
13
|
+
# this checker refuses output that would produce an invalid package.
|
|
14
|
+
#
|
|
15
|
+
# Issue codes mirror the post-hoc OpcValidator semantics
|
|
16
|
+
# (lib/uniword/validation/opc_validator.rb) where they match:
|
|
17
|
+
# - OPC-005: every ZIP entry has a content type (Default or Override)
|
|
18
|
+
# - OPC-006: every relationship target resolves to a package entry
|
|
19
|
+
# - OPC-008: every emitted XML part is well-formed
|
|
20
|
+
# Codes introduced by the write-time gate:
|
|
21
|
+
# - OPC-009: every r:id/r:embed/r:link reference in any XML part
|
|
22
|
+
# resolves to a Relationship in that part's .rels
|
|
23
|
+
# - OPC-010: relationship IDs are unique within every .rels part
|
|
24
|
+
#
|
|
25
|
+
# @example Check an in-memory content hash
|
|
26
|
+
# issues = PackageIntegrityChecker.new.check(zip_content)
|
|
27
|
+
# issues.each { |i| puts "#{i.code} [#{i.part}] #{i.message}" }
|
|
28
|
+
class PackageIntegrityChecker
|
|
29
|
+
# Package relationships part namespace (OPC).
|
|
30
|
+
RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
|
31
|
+
|
|
32
|
+
# Office document relationship attribute namespace (r:id, r:embed...).
|
|
33
|
+
OFFICE_RELS_NS =
|
|
34
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
35
|
+
|
|
36
|
+
# [Content_Types].xml namespace.
|
|
37
|
+
CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
|
|
38
|
+
|
|
39
|
+
# Relationship attribute local names that reference rIds.
|
|
40
|
+
R_REF_ATTRIBUTES = %w[id embed link].freeze
|
|
41
|
+
|
|
42
|
+
# Check an in-memory package content hash against every invariant.
|
|
43
|
+
#
|
|
44
|
+
# One issue-collector pass; never mutates the content hash.
|
|
45
|
+
#
|
|
46
|
+
# @param content [Hash{String => String}] ZIP entry path => content
|
|
47
|
+
# @return [Array<Validation::Report::ValidationIssue>] Issues found
|
|
48
|
+
# (empty when the package is valid)
|
|
49
|
+
def check(content)
|
|
50
|
+
@content = content
|
|
51
|
+
@issues = []
|
|
52
|
+
@parsed_parts = {}
|
|
53
|
+
|
|
54
|
+
parse_all_parts # OPC-008, memoizes documents for later checks
|
|
55
|
+
check_content_type_coverage # OPC-005
|
|
56
|
+
check_relationship_targets # OPC-006
|
|
57
|
+
check_relationship_references # OPC-009
|
|
58
|
+
check_rid_uniqueness # OPC-010
|
|
59
|
+
|
|
60
|
+
@issues
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# -- Shared parsing -------------------------------------------------
|
|
66
|
+
|
|
67
|
+
# Entries whose content must be well-formed XML.
|
|
68
|
+
#
|
|
69
|
+
# @return [Array<String>] Entry paths ending in .xml or .rels
|
|
70
|
+
def parsable_parts
|
|
71
|
+
@content.keys.select do |name|
|
|
72
|
+
name.end_with?(".xml", ".rels")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Strict-parse every XML entry once, recording OPC-008 on failure.
|
|
77
|
+
#
|
|
78
|
+
# @return [void]
|
|
79
|
+
def parse_all_parts
|
|
80
|
+
parsable_parts.each { |name| parsed_part(name) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Memoized strict parse of one entry.
|
|
84
|
+
#
|
|
85
|
+
# @param name [String] Entry path
|
|
86
|
+
# @return [Nokogiri::XML::Document, nil] Parsed document, or nil when
|
|
87
|
+
# the entry is malformed (OPC-008 already recorded)
|
|
88
|
+
def parsed_part(name)
|
|
89
|
+
return @parsed_parts[name] if @parsed_parts.key?(name)
|
|
90
|
+
|
|
91
|
+
@parsed_parts[name] =
|
|
92
|
+
begin
|
|
93
|
+
Nokogiri::XML(@content[name], &:strict)
|
|
94
|
+
rescue Nokogiri::XML::SyntaxError => e
|
|
95
|
+
add_issue("OPC-008", name,
|
|
96
|
+
"Malformed XML in #{name}: #{e.message}")
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# -- OPC-005: content-type coverage ----------------------------------
|
|
102
|
+
|
|
103
|
+
def check_content_type_coverage
|
|
104
|
+
ct_name = "[Content_Types].xml"
|
|
105
|
+
unless @content.key?(ct_name)
|
|
106
|
+
add_issue("OPC-005", ct_name,
|
|
107
|
+
"#{ct_name} is missing; no entry has a declared " \
|
|
108
|
+
"content type")
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
ct_doc = parsed_part(ct_name)
|
|
113
|
+
return unless ct_doc # malformed already reported as OPC-008
|
|
114
|
+
|
|
115
|
+
defaults = ct_doc.xpath("//xmlns:Default", "xmlns" => CT_NS)
|
|
116
|
+
.filter_map { |node| node["Extension"] }.to_set
|
|
117
|
+
overrides = ct_doc.xpath("//xmlns:Override", "xmlns" => CT_NS)
|
|
118
|
+
.filter_map { |node| node["PartName"] }.to_set
|
|
119
|
+
|
|
120
|
+
@content.each_key do |name|
|
|
121
|
+
next if name.end_with?(".rels") # implicit content type per OPC
|
|
122
|
+
|
|
123
|
+
ext = File.extname(name)[1..]
|
|
124
|
+
next if ext && defaults.include?(ext)
|
|
125
|
+
next if overrides.include?("/#{name}")
|
|
126
|
+
|
|
127
|
+
add_issue("OPC-005", name,
|
|
128
|
+
"No content type declared for #{name}")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# -- OPC-006: relationship target existence --------------------------
|
|
133
|
+
|
|
134
|
+
def check_relationship_targets
|
|
135
|
+
rels_parts.each do |rels_name|
|
|
136
|
+
doc = parsed_part(rels_name)
|
|
137
|
+
next unless doc
|
|
138
|
+
|
|
139
|
+
base_dir = rels_base_dir(rels_name)
|
|
140
|
+
relationship_nodes(doc).each do |node|
|
|
141
|
+
target = node["Target"]
|
|
142
|
+
next if node["TargetMode"] == "External"
|
|
143
|
+
next unless target
|
|
144
|
+
next if target.start_with?("#")
|
|
145
|
+
|
|
146
|
+
resolved = resolve_target(base_dir, target)
|
|
147
|
+
next if @content.key?(resolved)
|
|
148
|
+
|
|
149
|
+
add_issue("OPC-006", rels_name,
|
|
150
|
+
"Relationship target not found: #{resolved} " \
|
|
151
|
+
"(referenced from #{rels_name})")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# -- OPC-009: r:id / r:embed / r:link resolution ---------------------
|
|
157
|
+
|
|
158
|
+
def check_relationship_references
|
|
159
|
+
@content.keys.each do |name|
|
|
160
|
+
next unless name.end_with?(".xml")
|
|
161
|
+
|
|
162
|
+
doc = parsed_part(name)
|
|
163
|
+
next unless doc
|
|
164
|
+
|
|
165
|
+
refs = relationship_references(doc)
|
|
166
|
+
next if refs.empty?
|
|
167
|
+
|
|
168
|
+
rels_name = rels_path_for(name)
|
|
169
|
+
valid_ids = relationship_ids(rels_name)
|
|
170
|
+
|
|
171
|
+
refs.each do |attr_name, rid|
|
|
172
|
+
next if valid_ids.include?(rid)
|
|
173
|
+
|
|
174
|
+
add_issue("OPC-009", name,
|
|
175
|
+
"Relationship reference r:#{attr_name}=\"#{rid}\" in " \
|
|
176
|
+
"#{name} has no matching Relationship in #{rels_name}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Collect (attribute local name, value) pairs for r:id/r:embed/r:link.
|
|
182
|
+
#
|
|
183
|
+
# @param doc [Nokogiri::XML::Document] Parsed XML part
|
|
184
|
+
# @return [Array<Array(String, String)>] Reference pairs
|
|
185
|
+
def relationship_references(doc)
|
|
186
|
+
refs = []
|
|
187
|
+
doc.traverse do |node|
|
|
188
|
+
next unless node.element?
|
|
189
|
+
|
|
190
|
+
node.attribute_nodes.each do |attr|
|
|
191
|
+
next unless attr.namespace&.href == OFFICE_RELS_NS
|
|
192
|
+
next unless R_REF_ATTRIBUTES.include?(attr.node_name)
|
|
193
|
+
|
|
194
|
+
value = attr.value
|
|
195
|
+
refs << [attr.node_name, value] unless value.empty?
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
refs
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Relationship IDs declared in one .rels entry.
|
|
202
|
+
#
|
|
203
|
+
# @param rels_name [String] .rels entry path
|
|
204
|
+
# @return [Set<String>] Declared relationship IDs (empty when the
|
|
205
|
+
# .rels entry is absent or malformed)
|
|
206
|
+
def relationship_ids(rels_name)
|
|
207
|
+
return Set.new unless @content.key?(rels_name)
|
|
208
|
+
|
|
209
|
+
doc = parsed_part(rels_name)
|
|
210
|
+
return Set.new unless doc
|
|
211
|
+
|
|
212
|
+
relationship_nodes(doc).filter_map { |node| node["Id"] }.to_set
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# -- OPC-010: rId uniqueness ------------------------------------------
|
|
216
|
+
|
|
217
|
+
def check_rid_uniqueness
|
|
218
|
+
rels_parts.each do |rels_name|
|
|
219
|
+
doc = parsed_part(rels_name)
|
|
220
|
+
next unless doc
|
|
221
|
+
|
|
222
|
+
seen = {}
|
|
223
|
+
relationship_nodes(doc).each do |node|
|
|
224
|
+
id = node["Id"]
|
|
225
|
+
next unless id
|
|
226
|
+
|
|
227
|
+
if seen[id]
|
|
228
|
+
add_issue("OPC-010", rels_name,
|
|
229
|
+
"Duplicate relationship ID #{id} in #{rels_name}")
|
|
230
|
+
else
|
|
231
|
+
seen[id] = true
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# -- Helpers ----------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
# @return [Array<String>] .rels entry paths in the content hash
|
|
240
|
+
def rels_parts
|
|
241
|
+
@content.keys.select { |name| name.end_with?(".rels") }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Relationship elements of a parsed .rels document.
|
|
245
|
+
#
|
|
246
|
+
# @param doc [Nokogiri::XML::Document] Parsed .rels part
|
|
247
|
+
# @return [Nokogiri::XML::NodeSet] Relationship elements
|
|
248
|
+
def relationship_nodes(doc)
|
|
249
|
+
doc.xpath("//xmlns:Relationship", "xmlns" => RELS_NS)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Base directory a .rels entry resolves targets against.
|
|
253
|
+
# "_rels/.rels" => "" (package root);
|
|
254
|
+
# "word/_rels/document.xml.rels" => "word".
|
|
255
|
+
#
|
|
256
|
+
# @param rels_name [String] .rels entry path
|
|
257
|
+
# @return [String] Base directory ("" for the package root)
|
|
258
|
+
def rels_base_dir(rels_name)
|
|
259
|
+
return "" if rels_name.start_with?("_rels/")
|
|
260
|
+
|
|
261
|
+
rels_name.sub(%r{/_rels/.*$}, "")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The .rels entry path belonging to a part.
|
|
265
|
+
# "word/document.xml" => "word/_rels/document.xml.rels".
|
|
266
|
+
#
|
|
267
|
+
# @param part_name [String] Part entry path
|
|
268
|
+
# @return [String] Corresponding .rels entry path
|
|
269
|
+
def rels_path_for(part_name)
|
|
270
|
+
dir = File.dirname(part_name)
|
|
271
|
+
File.join(dir, "_rels", "#{File.basename(part_name)}.rels")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Resolve a relationship target to a normalized entry path,
|
|
275
|
+
# handling leading-slash package-absolute targets and ".." segments.
|
|
276
|
+
#
|
|
277
|
+
# @param base_dir [String] Owning part's directory ("" for root)
|
|
278
|
+
# @param target [String] Raw Target attribute value
|
|
279
|
+
# @return [String] Normalized entry path
|
|
280
|
+
def resolve_target(base_dir, target)
|
|
281
|
+
return target[1..] if target.start_with?("/")
|
|
282
|
+
|
|
283
|
+
normalize_package_path(File.join(base_dir, target))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Lexically normalize a package-relative path (resolve "." and ".."
|
|
287
|
+
# segments). Deliberately avoids File.expand_path, which prepends a
|
|
288
|
+
# drive letter on Windows.
|
|
289
|
+
#
|
|
290
|
+
# @param path [String] Package-relative path
|
|
291
|
+
# @return [String] Normalized path
|
|
292
|
+
def normalize_package_path(path)
|
|
293
|
+
segments = []
|
|
294
|
+
path.split("/").each do |segment|
|
|
295
|
+
next if segment.empty? || segment == "."
|
|
296
|
+
|
|
297
|
+
segment == ".." ? segments.pop : segments << segment
|
|
298
|
+
end
|
|
299
|
+
segments.join("/")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def add_issue(code, part, message)
|
|
303
|
+
@issues << Validation::Report::ValidationIssue.new(
|
|
304
|
+
severity: "error",
|
|
305
|
+
code: code,
|
|
306
|
+
message: message,
|
|
307
|
+
part: part,
|
|
308
|
+
)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|