uniword 1.2.5 → 1.4.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 +313 -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 +22 -14
- 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 +11 -10
- 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 +12 -18
- 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 +91 -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/image_part.rb +83 -0
- data/lib/uniword/docx/package.rb +141 -333
- data/lib/uniword/docx/package_defaults.rb +83 -99
- data/lib/uniword/docx/package_integrity_checker.rb +312 -0
- data/lib/uniword/docx/package_serialization.rb +144 -233
- data/lib/uniword/docx/part.rb +125 -0
- data/lib/uniword/docx/part_collection.rb +131 -0
- data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
- data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
- data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
- data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
- data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
- data/lib/uniword/docx/part_loader/load_context.rb +85 -0
- data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
- data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
- data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
- data/lib/uniword/docx/part_loader.rb +92 -0
- data/lib/uniword/docx/raw_part.rb +75 -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 +18 -31
- data/lib/uniword/docx/reconciler/notes.rb +17 -8
- data/lib/uniword/docx/reconciler/package_structure.rb +168 -221
- data/lib/uniword/docx/reconciler/parts.rb +36 -13
- data/lib/uniword/docx/reconciler/referential_integrity.rb +222 -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 +13 -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/images/image_manager.rb +13 -13
- 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/element_order.rb +94 -0
- data/lib/uniword/ooxml/part_definition.rb +329 -0
- data/lib/uniword/ooxml/part_registry.rb +504 -0
- data/lib/uniword/ooxml/relationships/image_relationship.rb +6 -3
- 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 +3 -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 +6 -5
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
- data/lib/uniword/validation/engine.rb +35 -0
- data/lib/uniword/validation/link_checker.rb +1 -1
- data/lib/uniword/validation/opc_validator.rb +16 -2
- 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 +46 -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 +121 -122
- data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -1
- data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -1
- 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/numbering_elements.rb +0 -15
- 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 +5 -2
- data/lib/uniword.rb +27 -2
- metadata +52 -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
|
@@ -44,13 +44,13 @@ document_rels)
|
|
|
44
44
|
inject_bibliography(content_types, document_rels)
|
|
45
45
|
inject_custom_properties(content_types, package_rels)
|
|
46
46
|
inject_custom_xml(content_types)
|
|
47
|
-
|
|
48
|
-
inject_footers(content_types, document_rels)
|
|
49
|
-
inject_header_footer_parts(content_types, document_rels)
|
|
47
|
+
inject_header_footer_content_types(content_types)
|
|
50
48
|
inject_notes(content_types, document_rels)
|
|
49
|
+
inject_comments(content_types, document_rels)
|
|
51
50
|
inject_theme(content_types, document_rels)
|
|
52
51
|
inject_numbering(content_types, document_rels)
|
|
53
52
|
inject_embeddings(content_types, document_rels)
|
|
53
|
+
inject_raw_part_content_types(content_types)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# Serialize all package parts to XML and add to content hash
|
|
@@ -137,13 +137,12 @@ document_rels)
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Notes
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
serialize_part(endnotes)
|
|
140
|
+
serialize_notes(content)
|
|
141
|
+
|
|
142
|
+
# Comments
|
|
143
|
+
if comments
|
|
144
|
+
content["word/comments.xml"] =
|
|
145
|
+
serialize_part(comments)
|
|
147
146
|
end
|
|
148
147
|
|
|
149
148
|
# Bibliography sources
|
|
@@ -152,13 +151,14 @@ document_rels)
|
|
|
152
151
|
serialize_infrastructure(document.bibliography_sources)
|
|
153
152
|
end
|
|
154
153
|
|
|
155
|
-
# Headers and footers
|
|
156
|
-
serialize_headers(content)
|
|
157
|
-
serialize_footers(content)
|
|
154
|
+
# Headers and footers (unified store)
|
|
158
155
|
serialize_header_footer_parts(content)
|
|
159
156
|
|
|
160
157
|
# OLE/embedded object binaries
|
|
161
158
|
serialize_embeddings(content)
|
|
159
|
+
|
|
160
|
+
# Raw passthrough parts (unmodelled; byte-for-byte)
|
|
161
|
+
serialize_raw_parts(content)
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
# Serialize an OOXML document part with standard encoding, single-line output.
|
|
@@ -183,340 +183,251 @@ document_rels)
|
|
|
183
183
|
|
|
184
184
|
private
|
|
185
185
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# Shared pattern: ensure a part has both a content type override and
|
|
194
|
-
# a document relationship. Idempotent — skips if already present.
|
|
195
|
-
# When allocator is present, only adds content type (rels handled by allocator).
|
|
196
|
-
def ensure_content_type(content_types, rels, part_name:,
|
|
197
|
-
content_type:, rel_type:, target:)
|
|
198
|
-
unless content_types.overrides.any? { |o| o.part_name == part_name }
|
|
199
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
200
|
-
part_name: part_name, content_type: content_type,
|
|
201
|
-
)
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
return if allocator
|
|
205
|
-
return if rels.relationships.any? { |r| r.target == target }
|
|
186
|
+
# Shared pattern: ensure a part has a content type override.
|
|
187
|
+
# Idempotent — skips if already present. Relationships are owned
|
|
188
|
+
# by the Reconciler (assembled from the IdAllocator), never by
|
|
189
|
+
# the serializer. Part metadata comes from Ooxml::PartRegistry.
|
|
190
|
+
def ensure_content_type(content_types, definition)
|
|
191
|
+
part_name = definition.part_name
|
|
192
|
+
return if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
206
193
|
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
195
|
+
part_name: part_name, content_type: definition.content_type,
|
|
209
196
|
)
|
|
210
197
|
end
|
|
211
198
|
|
|
212
199
|
# Legacy alias — calls ensure_content_type (same behavior)
|
|
213
200
|
alias ensure_part_registered ensure_content_type
|
|
214
201
|
|
|
215
|
-
def inject_image_parts(content, content_types,
|
|
202
|
+
def inject_image_parts(content, content_types, _document_rels)
|
|
216
203
|
return unless document&.image_parts && !document.image_parts.empty?
|
|
217
204
|
|
|
218
|
-
document.image_parts.each_value do |
|
|
219
|
-
ext = File.extname(
|
|
205
|
+
document.image_parts.each_value do |part|
|
|
206
|
+
ext = File.extname(part.target.to_s).delete(".")
|
|
220
207
|
next if content_types.defaults.any? { |d| d.extension == ext }
|
|
221
208
|
|
|
222
209
|
content_types.defaults << Uniword::ContentTypes::Default.new(
|
|
223
|
-
extension: ext, content_type:
|
|
210
|
+
extension: ext, content_type: part.content_type,
|
|
224
211
|
)
|
|
225
212
|
end
|
|
226
213
|
|
|
227
|
-
document.image_parts.each_value do |
|
|
228
|
-
content["word/#{
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
return if allocator
|
|
232
|
-
|
|
233
|
-
document.image_parts.each do |r_id, image_data|
|
|
234
|
-
next if document_rels.relationships.any? { |r| r.target == image_data[:target] }
|
|
235
|
-
|
|
236
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
237
|
-
id: r_id,
|
|
238
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
239
|
-
target: image_data[:target],
|
|
240
|
-
)
|
|
214
|
+
document.image_parts.each_value do |part|
|
|
215
|
+
content["word/#{part.target}"] = part.data
|
|
241
216
|
end
|
|
242
217
|
end
|
|
243
218
|
|
|
244
|
-
def inject_chart_parts(content, content_types,
|
|
219
|
+
def inject_chart_parts(content, content_types, _document_rels)
|
|
245
220
|
return unless document&.chart_parts && !document.chart_parts.empty?
|
|
246
221
|
|
|
222
|
+
chart = Ooxml::PartRegistry.find_by_key(:chart)
|
|
247
223
|
unless content_types.overrides.any? do |o|
|
|
248
224
|
o.part_name&.start_with?("/word/charts/")
|
|
249
225
|
end
|
|
250
226
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
251
|
-
part_name:
|
|
252
|
-
content_type:
|
|
227
|
+
part_name: chart.part_name_for(n: 1),
|
|
228
|
+
content_type: chart.content_type,
|
|
253
229
|
)
|
|
254
230
|
end
|
|
255
231
|
|
|
256
|
-
document.chart_parts.
|
|
257
|
-
content["word/#{
|
|
258
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
259
|
-
id: r_id,
|
|
260
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
|
|
261
|
-
target: chart_data[:target],
|
|
262
|
-
)
|
|
232
|
+
document.chart_parts.each_value do |part|
|
|
233
|
+
content["word/#{part.target}"] = part.xml
|
|
263
234
|
end
|
|
264
235
|
end
|
|
265
236
|
|
|
266
|
-
def inject_bibliography(content_types,
|
|
237
|
+
def inject_bibliography(content_types, _document_rels)
|
|
267
238
|
return unless document&.bibliography_sources
|
|
268
239
|
|
|
269
|
-
ensure_content_type(content_types,
|
|
270
|
-
|
|
271
|
-
content_type: "application/vnd.openxmlformats-officedocument.bibliography+xml",
|
|
272
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/bibliography",
|
|
273
|
-
target: "sources.xml")
|
|
240
|
+
ensure_content_type(content_types,
|
|
241
|
+
Ooxml::PartRegistry.find_by_key(:bibliography))
|
|
274
242
|
end
|
|
275
243
|
|
|
276
244
|
def inject_custom_properties(content_types, package_rels)
|
|
277
245
|
return unless custom_properties && !custom_properties.properties.empty?
|
|
278
246
|
|
|
247
|
+
definition = Ooxml::PartRegistry.find_by_key(:custom_properties)
|
|
279
248
|
unless content_types.overrides.any? do |o|
|
|
280
|
-
o.part_name ==
|
|
249
|
+
o.part_name == definition.part_name
|
|
281
250
|
end
|
|
282
251
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
283
|
-
part_name:
|
|
284
|
-
content_type:
|
|
252
|
+
part_name: definition.part_name,
|
|
253
|
+
content_type: definition.content_type,
|
|
285
254
|
)
|
|
286
255
|
end
|
|
287
256
|
|
|
288
257
|
return if package_rels.relationships.any? do |r|
|
|
289
|
-
r.type.to_s.include?(
|
|
258
|
+
r.type.to_s.include?(definition.rel_type)
|
|
290
259
|
end
|
|
291
260
|
|
|
261
|
+
# The reconciler runs before injection; register the rId with
|
|
262
|
+
# the allocator (single authority) and append the rel here.
|
|
292
263
|
package_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
293
|
-
id:
|
|
294
|
-
|
|
295
|
-
|
|
264
|
+
id: allocator.alloc_rid(target: definition.target,
|
|
265
|
+
type: definition.rel_type,
|
|
266
|
+
scope: :package),
|
|
267
|
+
type: definition.rel_type,
|
|
268
|
+
target: definition.target,
|
|
296
269
|
)
|
|
297
270
|
end
|
|
298
271
|
|
|
299
272
|
def inject_custom_xml(content_types)
|
|
300
273
|
return unless custom_xml_items && !custom_xml_items.empty?
|
|
301
274
|
|
|
275
|
+
props = Ooxml::PartRegistry.find_by_key(:custom_xml_item_props)
|
|
302
276
|
custom_xml_items.each do |item|
|
|
303
277
|
idx = item[:index]
|
|
278
|
+
part_name = props.part_name_for(index: idx)
|
|
304
279
|
next if content_types.overrides.any? do |o|
|
|
305
|
-
o.part_name ==
|
|
280
|
+
o.part_name == part_name
|
|
306
281
|
end
|
|
307
282
|
|
|
308
283
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
309
|
-
part_name:
|
|
310
|
-
content_type:
|
|
311
|
-
)
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def inject_headers(content_types, document_rels)
|
|
316
|
-
return unless document&.headers && !document.headers.empty?
|
|
317
|
-
|
|
318
|
-
counter = 0
|
|
319
|
-
document.headers.each_key do |type|
|
|
320
|
-
counter += 1
|
|
321
|
-
target = "header#{counter}.xml"
|
|
322
|
-
|
|
323
|
-
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
324
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
325
|
-
part_name: "/word/#{target}",
|
|
326
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
|
327
|
-
)
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
next if allocator
|
|
331
|
-
next if document_rels.relationships.any? { |r| r.target == target }
|
|
332
|
-
|
|
333
|
-
r_id = next_rid(document_rels)
|
|
334
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
335
|
-
id: r_id,
|
|
336
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
|
|
337
|
-
target: target,
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
wire_header_reference(type, r_id)
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def inject_footers(content_types, document_rels)
|
|
345
|
-
return unless document&.footers && !document.footers.empty?
|
|
346
|
-
|
|
347
|
-
counter = 0
|
|
348
|
-
document.footers.each_key do |type|
|
|
349
|
-
counter += 1
|
|
350
|
-
target = "footer#{counter}.xml"
|
|
351
|
-
|
|
352
|
-
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
353
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
354
|
-
part_name: "/word/#{target}",
|
|
355
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
|
356
|
-
)
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
next if allocator
|
|
360
|
-
next if document_rels.relationships.any? { |r| r.target == target }
|
|
361
|
-
|
|
362
|
-
r_id = next_rid(document_rels)
|
|
363
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
364
|
-
id: r_id,
|
|
365
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
|
|
366
|
-
target: target,
|
|
284
|
+
part_name: part_name,
|
|
285
|
+
content_type: props.content_type,
|
|
367
286
|
)
|
|
368
|
-
|
|
369
|
-
wire_footer_reference(type, r_id)
|
|
370
287
|
end
|
|
371
288
|
end
|
|
372
289
|
|
|
373
|
-
|
|
374
|
-
|
|
290
|
+
# Register content-type overrides for every header/footer part in
|
|
291
|
+
# the unified store. Relationships and sectPr references are owned
|
|
292
|
+
# by the Reconciler (single wiring implementation); the serializer
|
|
293
|
+
# only ensures each emitted part has exactly one override.
|
|
294
|
+
def inject_header_footer_content_types(content_types)
|
|
295
|
+
parts = document&.header_footer_parts
|
|
296
|
+
return unless parts && !parts.empty?
|
|
375
297
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
unless content_types.overrides.any? { |o| o.part_name == part_name }
|
|
379
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
380
|
-
part_name: part_name,
|
|
381
|
-
content_type: part[:content_type],
|
|
382
|
-
)
|
|
383
|
-
end
|
|
298
|
+
parts.each do |part|
|
|
299
|
+
next unless part.target
|
|
384
300
|
|
|
385
|
-
|
|
386
|
-
next if
|
|
301
|
+
part_name = "/word/#{part.target}"
|
|
302
|
+
next if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
387
303
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
type: part[:rel_type],
|
|
391
|
-
target: part[:target],
|
|
304
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
305
|
+
part_name: part_name, content_type: part.content_type,
|
|
392
306
|
)
|
|
393
307
|
end
|
|
394
308
|
end
|
|
395
309
|
|
|
396
|
-
def inject_notes(content_types,
|
|
310
|
+
def inject_notes(content_types, _document_rels)
|
|
397
311
|
if footnotes
|
|
398
|
-
ensure_content_type(content_types,
|
|
399
|
-
|
|
400
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
401
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
|
|
402
|
-
target: "footnotes.xml")
|
|
312
|
+
ensure_content_type(content_types,
|
|
313
|
+
Ooxml::PartRegistry.find_by_key(:footnotes))
|
|
403
314
|
end
|
|
404
315
|
|
|
405
316
|
return unless endnotes
|
|
406
317
|
|
|
407
|
-
ensure_content_type(content_types,
|
|
408
|
-
|
|
409
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
410
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",
|
|
411
|
-
target: "endnotes.xml")
|
|
318
|
+
ensure_content_type(content_types,
|
|
319
|
+
Ooxml::PartRegistry.find_by_key(:endnotes))
|
|
412
320
|
end
|
|
413
321
|
|
|
414
|
-
def inject_theme(content_types,
|
|
322
|
+
def inject_theme(content_types, _document_rels)
|
|
415
323
|
return unless theme
|
|
416
324
|
|
|
417
|
-
ensure_content_type(content_types,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
325
|
+
ensure_content_type(content_types,
|
|
326
|
+
Ooxml::PartRegistry.find_by_key(:theme))
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def inject_comments(content_types, _document_rels)
|
|
330
|
+
return unless comments
|
|
331
|
+
|
|
332
|
+
ensure_content_type(content_types,
|
|
333
|
+
Ooxml::PartRegistry.find_by_key(:comments))
|
|
422
334
|
end
|
|
423
335
|
|
|
424
|
-
def inject_numbering(content_types,
|
|
336
|
+
def inject_numbering(content_types, _document_rels)
|
|
425
337
|
return unless numbering
|
|
426
338
|
|
|
427
|
-
ensure_content_type(content_types,
|
|
428
|
-
|
|
429
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
|
430
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
|
|
431
|
-
target: "numbering.xml")
|
|
339
|
+
ensure_content_type(content_types,
|
|
340
|
+
Ooxml::PartRegistry.find_by_key(:numbering))
|
|
432
341
|
end
|
|
433
342
|
|
|
434
|
-
def inject_embeddings(content_types,
|
|
343
|
+
def inject_embeddings(content_types, _document_rels)
|
|
435
344
|
return unless embeddings && !embeddings.empty?
|
|
436
345
|
|
|
437
|
-
|
|
346
|
+
ole = Ooxml::PartRegistry.find_by_key(:ole_object)
|
|
347
|
+
embeddings.each_key do |target|
|
|
438
348
|
part_name = "/word/#{target}"
|
|
439
349
|
next if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
440
350
|
|
|
441
351
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
442
352
|
part_name: part_name,
|
|
443
|
-
content_type:
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
next if document_rels.relationships.any? { |r| r.target == target }
|
|
447
|
-
|
|
448
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
449
|
-
id: "rIdEmbedding#{idx + 1}",
|
|
450
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject",
|
|
451
|
-
target: target,
|
|
353
|
+
content_type: ole.content_type,
|
|
452
354
|
)
|
|
453
355
|
end
|
|
454
356
|
end
|
|
455
357
|
|
|
456
|
-
def
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
358
|
+
def serialize_notes(content)
|
|
359
|
+
if footnotes
|
|
360
|
+
content["word/footnotes.xml"] =
|
|
361
|
+
serialize_part(footnotes)
|
|
362
|
+
end
|
|
363
|
+
if endnotes
|
|
364
|
+
content["word/endnotes.xml"] =
|
|
365
|
+
serialize_part(endnotes)
|
|
366
|
+
end
|
|
367
|
+
if footnotes_rels
|
|
368
|
+
content["word/_rels/footnotes.xml.rels"] =
|
|
369
|
+
serialize_infrastructure(footnotes_rels)
|
|
370
|
+
end
|
|
371
|
+
if endnotes_rels
|
|
372
|
+
content["word/_rels/endnotes.xml.rels"] =
|
|
373
|
+
serialize_infrastructure(endnotes_rels)
|
|
467
374
|
end
|
|
468
375
|
end
|
|
469
376
|
|
|
470
|
-
|
|
471
|
-
|
|
377
|
+
# Emit every header/footer part from the unified store: one part
|
|
378
|
+
# file per part, headers before footers (historic emission order).
|
|
379
|
+
def serialize_header_footer_parts(content)
|
|
380
|
+
parts = document&.header_footer_parts
|
|
381
|
+
return unless parts && !parts.empty?
|
|
382
|
+
|
|
383
|
+
(parts.of_kind(:header) + parts.of_kind(:footer)).each do |part|
|
|
384
|
+
next unless part.target && part.content
|
|
472
385
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if existing
|
|
476
|
-
existing.r_id = r_id
|
|
477
|
-
else
|
|
478
|
-
sect_pr.footer_references << Wordprocessingml::FooterReference.new(
|
|
479
|
-
type: type, r_id: r_id,
|
|
480
|
-
)
|
|
386
|
+
content["word/#{part.target}"] =
|
|
387
|
+
serialize_part(part.serializable_content)
|
|
481
388
|
end
|
|
482
389
|
end
|
|
483
390
|
|
|
484
|
-
def
|
|
485
|
-
return unless
|
|
391
|
+
def serialize_embeddings(content)
|
|
392
|
+
return unless embeddings && !embeddings.empty?
|
|
486
393
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
idx += 1
|
|
490
|
-
content["word/header#{idx}.xml"] =
|
|
491
|
-
serialize_part(header_obj)
|
|
394
|
+
embeddings.each do |target, part|
|
|
395
|
+
content["word/#{target}"] = part.content
|
|
492
396
|
end
|
|
493
397
|
end
|
|
494
398
|
|
|
495
|
-
|
|
496
|
-
|
|
399
|
+
# Content types for raw-carried parts. Source Overrides survive
|
|
400
|
+
# reconciliation untouched (non-standard entries are preserved);
|
|
401
|
+
# a part whose content type came from a Default entry needs an
|
|
402
|
+
# Override once the reconciler rebuilds the standard Defaults.
|
|
403
|
+
def inject_raw_part_content_types(content_types)
|
|
404
|
+
return if raw_parts.empty?
|
|
497
405
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
idx += 1
|
|
501
|
-
content["word/footer#{idx}.xml"] =
|
|
502
|
-
serialize_part(footer_obj)
|
|
503
|
-
end
|
|
504
|
-
end
|
|
406
|
+
raw_parts.each_value do |part|
|
|
407
|
+
next unless part.path && part.content_type
|
|
505
408
|
|
|
506
|
-
|
|
507
|
-
|
|
409
|
+
ext = File.extname(part.path)[1..]
|
|
410
|
+
next if ext && content_types.defaults.any? do |d|
|
|
411
|
+
d.extension == ext
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
part_name = "/#{part.path}"
|
|
415
|
+
next if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
508
416
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
417
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
418
|
+
part_name: part_name, content_type: part.content_type,
|
|
419
|
+
)
|
|
512
420
|
end
|
|
513
421
|
end
|
|
514
422
|
|
|
515
|
-
|
|
516
|
-
|
|
423
|
+
# Emit raw-carried parts byte-for-byte. Model emission always
|
|
424
|
+
# wins: raw claiming already excludes registry paths, and the
|
|
425
|
+
# key guard makes double emission impossible by construction.
|
|
426
|
+
def serialize_raw_parts(content)
|
|
427
|
+
return if raw_parts.empty?
|
|
517
428
|
|
|
518
|
-
|
|
519
|
-
content[
|
|
429
|
+
raw_parts.each do |path, part|
|
|
430
|
+
content[path] = part.content unless content.key?(path)
|
|
520
431
|
end
|
|
521
432
|
end
|
|
522
433
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# Value object for one package-held part (chart, embedding,
|
|
6
|
+
# header, footer, ...).
|
|
7
|
+
#
|
|
8
|
+
# A Part couples the part's content (a lutaml-model object or a
|
|
9
|
+
# raw String) with the packaging metadata the OPC layer needs to
|
|
10
|
+
# emit it: relationship target, relationship id, content type and
|
|
11
|
+
# relationship type. Defaults for the metadata come from the
|
|
12
|
+
# part's Ooxml::PartDefinition (the single source of truth);
|
|
13
|
+
# loaded documents may carry verbatim overrides (e.g. strict-OOXML
|
|
14
|
+
# relationship URIs).
|
|
15
|
+
#
|
|
16
|
+
# Replaces the raw Hash entries previously stored in
|
|
17
|
+
# +chart_parts+, +embeddings+ and +header_footer_parts+. Hash-style
|
|
18
|
+
# read access (+part[:target]+) is kept for backward compatibility.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# part = Part.new(
|
|
22
|
+
# definition: Ooxml::PartRegistry.find_by_key(:ole_object),
|
|
23
|
+
# target: "embeddings/file.xlsx",
|
|
24
|
+
# content: binary_data,
|
|
25
|
+
# )
|
|
26
|
+
# part.path # => "word/embeddings/file.xlsx"
|
|
27
|
+
# part.content_type # => definition's content type
|
|
28
|
+
class Part
|
|
29
|
+
# @return [Ooxml::PartDefinition, nil] registry definition for
|
|
30
|
+
# this part kind
|
|
31
|
+
attr_reader :definition
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] relationship id (nil until wired)
|
|
34
|
+
attr_accessor :r_id
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] relationship target relative to word/
|
|
37
|
+
attr_accessor :target
|
|
38
|
+
|
|
39
|
+
# @return [Object] part content (model object or raw String)
|
|
40
|
+
attr_accessor :content
|
|
41
|
+
|
|
42
|
+
# @param definition [Ooxml::PartDefinition, nil] registry entry
|
|
43
|
+
# @param r_id [String, nil] relationship id
|
|
44
|
+
# @param target [String, nil] relationship target (relative to
|
|
45
|
+
# the owning part, e.g. "charts/chart1.xml")
|
|
46
|
+
# @param content [Object, nil] part content
|
|
47
|
+
# @param rel_type [String, nil] explicit relationship type
|
|
48
|
+
# override (verbatim value from a loaded package)
|
|
49
|
+
# @param content_type [String, nil] explicit content type
|
|
50
|
+
# override (verbatim value from a loaded package)
|
|
51
|
+
# rubocop:disable Metrics/ParameterLists
|
|
52
|
+
def initialize(definition: nil, r_id: nil, target: nil, content: nil,
|
|
53
|
+
rel_type: nil, content_type: nil)
|
|
54
|
+
@definition = definition
|
|
55
|
+
@r_id = r_id
|
|
56
|
+
@target = target
|
|
57
|
+
@content = content
|
|
58
|
+
@rel_type = rel_type
|
|
59
|
+
@content_type = content_type
|
|
60
|
+
end
|
|
61
|
+
# rubocop:enable Metrics/ParameterLists
|
|
62
|
+
|
|
63
|
+
# Wrap a legacy raw-hash entry ({ content:/xml:, target: }) into
|
|
64
|
+
# a Part. Subclasses override for family-specific keys (e.g.
|
|
65
|
+
# ImagePart's { data:, content_type:, path: }). Used by
|
|
66
|
+
# PartCollection to normalize hash assignments.
|
|
67
|
+
#
|
|
68
|
+
# @param hash [Hash] legacy hash entry
|
|
69
|
+
# @return [Part]
|
|
70
|
+
def self.from_hash(hash)
|
|
71
|
+
new(target: hash[:target], content: hash[:content] || hash[:xml])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [String, nil] relationship type URI
|
|
75
|
+
def rel_type
|
|
76
|
+
@rel_type || definition&.rel_type
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [String, nil] MIME content type
|
|
80
|
+
def content_type
|
|
81
|
+
@content_type || definition&.content_type
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Package-relative path of the emitted part. Document-scoped
|
|
85
|
+
# parts live under word/; their target is already relative to
|
|
86
|
+
# word/.
|
|
87
|
+
#
|
|
88
|
+
# @return [String, nil] e.g. "word/charts/chart1.xml"
|
|
89
|
+
def path
|
|
90
|
+
target && "word/#{target}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Package paths this part emits (one for plain parts; subclasses
|
|
94
|
+
# with sidecar parts override).
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<String>] emitted package paths
|
|
97
|
+
def package_paths
|
|
98
|
+
[path].compact
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Verbatim overrides carried from a loaded package.
|
|
102
|
+
attr_writer :rel_type, :content_type
|
|
103
|
+
|
|
104
|
+
# Key → reader lambdas backing hash-style access (kept as data
|
|
105
|
+
# so the dispatch stays declarative).
|
|
106
|
+
HASH_LOOKUP = {
|
|
107
|
+
r_id: lambda(&:r_id),
|
|
108
|
+
target: lambda(&:target),
|
|
109
|
+
rel_type: lambda(&:rel_type),
|
|
110
|
+
content_type: lambda(&:content_type),
|
|
111
|
+
content: lambda(&:content),
|
|
112
|
+
path: lambda(&:path),
|
|
113
|
+
}.freeze
|
|
114
|
+
|
|
115
|
+
# Hash-style read compatibility for former hash entries.
|
|
116
|
+
#
|
|
117
|
+
# @param key [Symbol, String] one of :r_id, :target, :rel_type,
|
|
118
|
+
# :content_type, :content, :path
|
|
119
|
+
# @return [Object, nil]
|
|
120
|
+
def [](key)
|
|
121
|
+
HASH_LOOKUP[key.to_sym]&.call(self)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|