uniword 1.2.4 → 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 +21 -9
- data/lib/uniword/builder/comment_anchorer.rb +221 -0
- data/lib/uniword/builder/document_builder.rb +97 -6
- data/lib/uniword/builder/has_borders.rb +8 -1
- 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 -24
- 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 +132 -38
- 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 +116 -234
- 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 +42 -16
- 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 +40 -15
- data/lib/uniword/docx.rb +10 -0
- data/lib/uniword/drawingml/blip.rb +0 -1
- data/lib/uniword/drawingml/color_scheme.rb +5 -7
- data/lib/uniword/drawingml/font_scheme.rb +4 -8
- data/lib/uniword/drawingml/theme.rb +12 -7
- 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 -4
- 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/resource/theme_processor.rb +1 -1
- 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 -3
- data/lib/uniword/themes/theme_transformation.rb +56 -42
- data/lib/uniword/toc/toc_generator.rb +2 -0
- data/lib/uniword/validation/checkers/footnote_reference_checker.rb +4 -1
- 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 +8 -1
- 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.rb +1 -1
- 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/run_properties.rb +20 -50
- 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 +4 -3
- data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
- data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
- data/lib/uniword/wordprocessingml/table_properties.rb +10 -15
- 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 -229
- data/lib/uniword/warnings/warning_report.rb +0 -159
- data/lib/uniword/warnings.rb +0 -9
|
@@ -44,10 +44,9 @@ 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)
|
|
@@ -69,11 +68,11 @@ document_rels)
|
|
|
69
68
|
end
|
|
70
69
|
if app_properties
|
|
71
70
|
content["docProps/app.xml"] =
|
|
72
|
-
app_properties.to_xml(DOCX_PROPS_OPTIONS.dup)
|
|
71
|
+
add_standalone(app_properties.to_xml(DOCX_PROPS_OPTIONS.dup))
|
|
73
72
|
end
|
|
74
73
|
if custom_properties
|
|
75
74
|
content["docProps/custom.xml"] =
|
|
76
|
-
custom_properties.to_xml(DOCX_PROPS_OPTIONS.dup)
|
|
75
|
+
add_standalone(custom_properties.to_xml(DOCX_PROPS_OPTIONS.dup))
|
|
77
76
|
end
|
|
78
77
|
|
|
79
78
|
# Custom XML data items
|
|
@@ -137,13 +136,12 @@ document_rels)
|
|
|
137
136
|
end
|
|
138
137
|
|
|
139
138
|
# Notes
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
serialize_part(endnotes)
|
|
139
|
+
serialize_notes(content)
|
|
140
|
+
|
|
141
|
+
# Comments
|
|
142
|
+
if comments
|
|
143
|
+
content["word/comments.xml"] =
|
|
144
|
+
serialize_part(comments)
|
|
147
145
|
end
|
|
148
146
|
|
|
149
147
|
# Bibliography sources
|
|
@@ -152,57 +150,52 @@ document_rels)
|
|
|
152
150
|
serialize_infrastructure(document.bibliography_sources)
|
|
153
151
|
end
|
|
154
152
|
|
|
155
|
-
# Headers and footers
|
|
156
|
-
serialize_headers(content)
|
|
157
|
-
serialize_footers(content)
|
|
153
|
+
# Headers and footers (unified store)
|
|
158
154
|
serialize_header_footer_parts(content)
|
|
159
155
|
|
|
160
156
|
# OLE/embedded object binaries
|
|
161
157
|
serialize_embeddings(content)
|
|
162
158
|
end
|
|
163
159
|
|
|
164
|
-
# Serialize an OOXML document part with standard encoding, single-line output
|
|
160
|
+
# Serialize an OOXML document part with standard encoding, single-line output.
|
|
161
|
+
# All DOCX parts require standalone="yes" in the XML declaration for Word compatibility.
|
|
165
162
|
def serialize_part(model)
|
|
166
|
-
model.to_xml(DOCX_PART_OPTIONS.dup)
|
|
163
|
+
add_standalone(model.to_xml(DOCX_PART_OPTIONS.dup))
|
|
167
164
|
end
|
|
168
165
|
|
|
169
166
|
# Serialize package infrastructure (rels, content types) with declaration, single-line output
|
|
170
167
|
def serialize_infrastructure(model)
|
|
171
|
-
model.to_xml(DOCX_INFRA_OPTIONS.dup)
|
|
168
|
+
add_standalone(model.to_xml(DOCX_INFRA_OPTIONS.dup))
|
|
172
169
|
end
|
|
173
170
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
# Ensure the XML declaration includes standalone="yes".
|
|
172
|
+
# lutaml-model omits this by default; Word's strict OPC validation requires it.
|
|
173
|
+
def add_standalone(xml)
|
|
174
|
+
xml.sub(
|
|
175
|
+
%r{<\?xml version="1.0" encoding="UTF-8"\?>},
|
|
176
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
|
|
180
177
|
)
|
|
181
178
|
end
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
# a document relationship. Idempotent — skips if already present.
|
|
185
|
-
# When allocator is present, only adds content type (rels handled by allocator).
|
|
186
|
-
def ensure_content_type(content_types, rels, part_name:,
|
|
187
|
-
content_type:, rel_type:, target:)
|
|
188
|
-
unless content_types.overrides.any? { |o| o.part_name == part_name }
|
|
189
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
190
|
-
part_name: part_name, content_type: content_type,
|
|
191
|
-
)
|
|
192
|
-
end
|
|
180
|
+
private
|
|
193
181
|
|
|
194
|
-
|
|
195
|
-
|
|
182
|
+
# Shared pattern: ensure a part has a content type override.
|
|
183
|
+
# Idempotent — skips if already present. Relationships are owned
|
|
184
|
+
# by the Reconciler (assembled from the IdAllocator), never by
|
|
185
|
+
# the serializer. Part metadata comes from Ooxml::PartRegistry.
|
|
186
|
+
def ensure_content_type(content_types, definition)
|
|
187
|
+
part_name = definition.part_name
|
|
188
|
+
return if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
196
189
|
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
191
|
+
part_name: part_name, content_type: definition.content_type,
|
|
199
192
|
)
|
|
200
193
|
end
|
|
201
194
|
|
|
202
195
|
# Legacy alias — calls ensure_content_type (same behavior)
|
|
203
196
|
alias ensure_part_registered ensure_content_type
|
|
204
197
|
|
|
205
|
-
def inject_image_parts(content, content_types,
|
|
198
|
+
def inject_image_parts(content, content_types, _document_rels)
|
|
206
199
|
return unless document&.image_parts && !document.image_parts.empty?
|
|
207
200
|
|
|
208
201
|
document.image_parts.each_value do |image_data|
|
|
@@ -217,296 +210,185 @@ document_rels)
|
|
|
217
210
|
document.image_parts.each_value do |image_data|
|
|
218
211
|
content["word/#{image_data[:target]}"] = image_data[:data]
|
|
219
212
|
end
|
|
220
|
-
|
|
221
|
-
return if allocator
|
|
222
|
-
|
|
223
|
-
document.image_parts.each do |r_id, image_data|
|
|
224
|
-
next if document_rels.relationships.any? { |r| r.target == image_data[:target] }
|
|
225
|
-
|
|
226
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
227
|
-
id: r_id,
|
|
228
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
229
|
-
target: image_data[:target],
|
|
230
|
-
)
|
|
231
|
-
end
|
|
232
213
|
end
|
|
233
214
|
|
|
234
|
-
def inject_chart_parts(content, content_types,
|
|
215
|
+
def inject_chart_parts(content, content_types, _document_rels)
|
|
235
216
|
return unless document&.chart_parts && !document.chart_parts.empty?
|
|
236
217
|
|
|
218
|
+
chart = Ooxml::PartRegistry.find_by_key(:chart)
|
|
237
219
|
unless content_types.overrides.any? do |o|
|
|
238
220
|
o.part_name&.start_with?("/word/charts/")
|
|
239
221
|
end
|
|
240
222
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
241
|
-
part_name:
|
|
242
|
-
content_type:
|
|
223
|
+
part_name: chart.part_name_for(n: 1),
|
|
224
|
+
content_type: chart.content_type,
|
|
243
225
|
)
|
|
244
226
|
end
|
|
245
227
|
|
|
246
|
-
document.chart_parts.
|
|
228
|
+
document.chart_parts.each_value do |chart_data|
|
|
247
229
|
content["word/#{chart_data[:target]}"] = chart_data[:xml]
|
|
248
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
249
|
-
id: r_id,
|
|
250
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
|
|
251
|
-
target: chart_data[:target],
|
|
252
|
-
)
|
|
253
230
|
end
|
|
254
231
|
end
|
|
255
232
|
|
|
256
|
-
def inject_bibliography(content_types,
|
|
233
|
+
def inject_bibliography(content_types, _document_rels)
|
|
257
234
|
return unless document&.bibliography_sources
|
|
258
235
|
|
|
259
|
-
ensure_content_type(content_types,
|
|
260
|
-
|
|
261
|
-
content_type: "application/vnd.openxmlformats-officedocument.bibliography+xml",
|
|
262
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/bibliography",
|
|
263
|
-
target: "sources.xml")
|
|
236
|
+
ensure_content_type(content_types,
|
|
237
|
+
Ooxml::PartRegistry.find_by_key(:bibliography))
|
|
264
238
|
end
|
|
265
239
|
|
|
266
240
|
def inject_custom_properties(content_types, package_rels)
|
|
267
241
|
return unless custom_properties && !custom_properties.properties.empty?
|
|
268
242
|
|
|
243
|
+
definition = Ooxml::PartRegistry.find_by_key(:custom_properties)
|
|
269
244
|
unless content_types.overrides.any? do |o|
|
|
270
|
-
o.part_name ==
|
|
245
|
+
o.part_name == definition.part_name
|
|
271
246
|
end
|
|
272
247
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
273
|
-
part_name:
|
|
274
|
-
content_type:
|
|
248
|
+
part_name: definition.part_name,
|
|
249
|
+
content_type: definition.content_type,
|
|
275
250
|
)
|
|
276
251
|
end
|
|
277
252
|
|
|
278
253
|
return if package_rels.relationships.any? do |r|
|
|
279
|
-
r.type.to_s.include?(
|
|
254
|
+
r.type.to_s.include?(definition.rel_type)
|
|
280
255
|
end
|
|
281
256
|
|
|
257
|
+
# The reconciler runs before injection; register the rId with
|
|
258
|
+
# the allocator (single authority) and append the rel here.
|
|
282
259
|
package_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
283
|
-
id:
|
|
284
|
-
|
|
285
|
-
|
|
260
|
+
id: allocator.alloc_rid(target: definition.target,
|
|
261
|
+
type: definition.rel_type,
|
|
262
|
+
scope: :package),
|
|
263
|
+
type: definition.rel_type,
|
|
264
|
+
target: definition.target,
|
|
286
265
|
)
|
|
287
266
|
end
|
|
288
267
|
|
|
289
268
|
def inject_custom_xml(content_types)
|
|
290
269
|
return unless custom_xml_items && !custom_xml_items.empty?
|
|
291
270
|
|
|
271
|
+
props = Ooxml::PartRegistry.find_by_key(:custom_xml_item_props)
|
|
292
272
|
custom_xml_items.each do |item|
|
|
293
273
|
idx = item[:index]
|
|
274
|
+
part_name = props.part_name_for(index: idx)
|
|
294
275
|
next if content_types.overrides.any? do |o|
|
|
295
|
-
o.part_name ==
|
|
276
|
+
o.part_name == part_name
|
|
296
277
|
end
|
|
297
278
|
|
|
298
279
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
299
|
-
part_name:
|
|
300
|
-
content_type:
|
|
301
|
-
)
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def inject_headers(content_types, document_rels)
|
|
306
|
-
return unless document&.headers && !document.headers.empty?
|
|
307
|
-
|
|
308
|
-
counter = 0
|
|
309
|
-
document.headers.each_key do |type|
|
|
310
|
-
counter += 1
|
|
311
|
-
target = "header#{counter}.xml"
|
|
312
|
-
|
|
313
|
-
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
314
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
315
|
-
part_name: "/word/#{target}",
|
|
316
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
|
317
|
-
)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
next if allocator
|
|
321
|
-
next if document_rels.relationships.any? { |r| r.target == target }
|
|
322
|
-
|
|
323
|
-
r_id = next_rid(document_rels)
|
|
324
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
325
|
-
id: r_id,
|
|
326
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
|
|
327
|
-
target: target,
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
wire_header_reference(type, r_id)
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def inject_footers(content_types, document_rels)
|
|
335
|
-
return unless document&.footers && !document.footers.empty?
|
|
336
|
-
|
|
337
|
-
counter = 0
|
|
338
|
-
document.footers.each_key do |type|
|
|
339
|
-
counter += 1
|
|
340
|
-
target = "footer#{counter}.xml"
|
|
341
|
-
|
|
342
|
-
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
343
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
344
|
-
part_name: "/word/#{target}",
|
|
345
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
|
346
|
-
)
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
next if allocator
|
|
350
|
-
next if document_rels.relationships.any? { |r| r.target == target }
|
|
351
|
-
|
|
352
|
-
r_id = next_rid(document_rels)
|
|
353
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
354
|
-
id: r_id,
|
|
355
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
|
|
356
|
-
target: target,
|
|
280
|
+
part_name: part_name,
|
|
281
|
+
content_type: props.content_type,
|
|
357
282
|
)
|
|
358
|
-
|
|
359
|
-
wire_footer_reference(type, r_id)
|
|
360
283
|
end
|
|
361
284
|
end
|
|
362
285
|
|
|
363
|
-
|
|
364
|
-
|
|
286
|
+
# Register content-type overrides for every header/footer part in
|
|
287
|
+
# the unified store. Relationships and sectPr references are owned
|
|
288
|
+
# by the Reconciler (single wiring implementation); the serializer
|
|
289
|
+
# only ensures each emitted part has exactly one override.
|
|
290
|
+
def inject_header_footer_content_types(content_types)
|
|
291
|
+
parts = document&.header_footer_parts
|
|
292
|
+
return unless parts && !parts.empty?
|
|
365
293
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
unless content_types.overrides.any? { |o| o.part_name == part_name }
|
|
369
|
-
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
370
|
-
part_name: part_name,
|
|
371
|
-
content_type: part[:content_type],
|
|
372
|
-
)
|
|
373
|
-
end
|
|
294
|
+
parts.each do |part|
|
|
295
|
+
next unless part.target
|
|
374
296
|
|
|
375
|
-
|
|
376
|
-
next if
|
|
297
|
+
part_name = "/word/#{part.target}"
|
|
298
|
+
next if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
377
299
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
type: part[:rel_type],
|
|
381
|
-
target: part[:target],
|
|
300
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
301
|
+
part_name: part_name, content_type: part.content_type,
|
|
382
302
|
)
|
|
383
303
|
end
|
|
384
304
|
end
|
|
385
305
|
|
|
386
|
-
def inject_notes(content_types,
|
|
306
|
+
def inject_notes(content_types, _document_rels)
|
|
387
307
|
if footnotes
|
|
388
|
-
ensure_content_type(content_types,
|
|
389
|
-
|
|
390
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
391
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
|
|
392
|
-
target: "footnotes.xml")
|
|
308
|
+
ensure_content_type(content_types,
|
|
309
|
+
Ooxml::PartRegistry.find_by_key(:footnotes))
|
|
393
310
|
end
|
|
394
311
|
|
|
395
312
|
return unless endnotes
|
|
396
313
|
|
|
397
|
-
ensure_content_type(content_types,
|
|
398
|
-
|
|
399
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
400
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",
|
|
401
|
-
target: "endnotes.xml")
|
|
314
|
+
ensure_content_type(content_types,
|
|
315
|
+
Ooxml::PartRegistry.find_by_key(:endnotes))
|
|
402
316
|
end
|
|
403
317
|
|
|
404
|
-
def inject_theme(content_types,
|
|
318
|
+
def inject_theme(content_types, _document_rels)
|
|
405
319
|
return unless theme
|
|
406
320
|
|
|
407
|
-
ensure_content_type(content_types,
|
|
408
|
-
|
|
409
|
-
content_type: "application/vnd.openxmlformats-officedocument.theme+xml",
|
|
410
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
|
|
411
|
-
target: "theme/theme1.xml")
|
|
321
|
+
ensure_content_type(content_types,
|
|
322
|
+
Ooxml::PartRegistry.find_by_key(:theme))
|
|
412
323
|
end
|
|
413
324
|
|
|
414
|
-
def
|
|
325
|
+
def inject_comments(content_types, _document_rels)
|
|
326
|
+
return unless comments
|
|
327
|
+
|
|
328
|
+
ensure_content_type(content_types,
|
|
329
|
+
Ooxml::PartRegistry.find_by_key(:comments))
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def inject_numbering(content_types, _document_rels)
|
|
415
333
|
return unless numbering
|
|
416
334
|
|
|
417
|
-
ensure_content_type(content_types,
|
|
418
|
-
|
|
419
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
|
420
|
-
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
|
|
421
|
-
target: "numbering.xml")
|
|
335
|
+
ensure_content_type(content_types,
|
|
336
|
+
Ooxml::PartRegistry.find_by_key(:numbering))
|
|
422
337
|
end
|
|
423
338
|
|
|
424
|
-
def inject_embeddings(content_types,
|
|
339
|
+
def inject_embeddings(content_types, _document_rels)
|
|
425
340
|
return unless embeddings && !embeddings.empty?
|
|
426
341
|
|
|
427
|
-
|
|
342
|
+
ole = Ooxml::PartRegistry.find_by_key(:ole_object)
|
|
343
|
+
embeddings.each_key do |target|
|
|
428
344
|
part_name = "/word/#{target}"
|
|
429
345
|
next if content_types.overrides.any? { |o| o.part_name == part_name }
|
|
430
346
|
|
|
431
347
|
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
432
348
|
part_name: part_name,
|
|
433
|
-
content_type:
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
next if document_rels.relationships.any? { |r| r.target == target }
|
|
437
|
-
|
|
438
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
439
|
-
id: "rIdEmbedding#{idx + 1}",
|
|
440
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject",
|
|
441
|
-
target: target,
|
|
349
|
+
content_type: ole.content_type,
|
|
442
350
|
)
|
|
443
351
|
end
|
|
444
352
|
end
|
|
445
353
|
|
|
446
|
-
def
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
existing = sect_pr.header_references&.find { |r| r.type == type }
|
|
451
|
-
if existing
|
|
452
|
-
existing.r_id = r_id
|
|
453
|
-
else
|
|
454
|
-
sect_pr.header_references << Wordprocessingml::HeaderReference.new(
|
|
455
|
-
type: type, r_id: r_id,
|
|
456
|
-
)
|
|
354
|
+
def serialize_notes(content)
|
|
355
|
+
if footnotes
|
|
356
|
+
content["word/footnotes.xml"] =
|
|
357
|
+
serialize_part(footnotes)
|
|
457
358
|
end
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return unless document&.body
|
|
462
|
-
|
|
463
|
-
sect_pr = document.body.section_properties ||= Wordprocessingml::SectionProperties.new
|
|
464
|
-
existing = sect_pr.footer_references&.find { |r| r.type == type }
|
|
465
|
-
if existing
|
|
466
|
-
existing.r_id = r_id
|
|
467
|
-
else
|
|
468
|
-
sect_pr.footer_references << Wordprocessingml::FooterReference.new(
|
|
469
|
-
type: type, r_id: r_id,
|
|
470
|
-
)
|
|
359
|
+
if endnotes
|
|
360
|
+
content["word/endnotes.xml"] =
|
|
361
|
+
serialize_part(endnotes)
|
|
471
362
|
end
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return unless document&.headers && !document.headers.empty?
|
|
476
|
-
|
|
477
|
-
idx = 0
|
|
478
|
-
document.headers.each_value do |header_obj|
|
|
479
|
-
idx += 1
|
|
480
|
-
content["word/header#{idx}.xml"] =
|
|
481
|
-
serialize_part(header_obj)
|
|
363
|
+
if footnotes_rels
|
|
364
|
+
content["word/_rels/footnotes.xml.rels"] =
|
|
365
|
+
serialize_infrastructure(footnotes_rels)
|
|
482
366
|
end
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return unless document&.footers && !document.footers.empty?
|
|
487
|
-
|
|
488
|
-
idx = 0
|
|
489
|
-
document.footers.each_value do |footer_obj|
|
|
490
|
-
idx += 1
|
|
491
|
-
content["word/footer#{idx}.xml"] =
|
|
492
|
-
serialize_part(footer_obj)
|
|
367
|
+
if endnotes_rels
|
|
368
|
+
content["word/_rels/endnotes.xml.rels"] =
|
|
369
|
+
serialize_infrastructure(endnotes_rels)
|
|
493
370
|
end
|
|
494
371
|
end
|
|
495
372
|
|
|
373
|
+
# Emit every header/footer part from the unified store: one part
|
|
374
|
+
# file per part, headers before footers (historic emission order).
|
|
496
375
|
def serialize_header_footer_parts(content)
|
|
497
|
-
|
|
376
|
+
parts = document&.header_footer_parts
|
|
377
|
+
return unless parts && !parts.empty?
|
|
378
|
+
|
|
379
|
+
(parts.of_kind(:header) + parts.of_kind(:footer)).each do |part|
|
|
380
|
+
next unless part.target && part.content
|
|
498
381
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
serialize_part(part[:content])
|
|
382
|
+
content["word/#{part.target}"] =
|
|
383
|
+
serialize_part(part.serializable_content)
|
|
502
384
|
end
|
|
503
385
|
end
|
|
504
386
|
|
|
505
387
|
def serialize_embeddings(content)
|
|
506
388
|
return unless embeddings && !embeddings.empty?
|
|
507
389
|
|
|
508
|
-
embeddings.each do |target,
|
|
509
|
-
content["word/#{target}"] =
|
|
390
|
+
embeddings.each do |target, part|
|
|
391
|
+
content["word/#{target}"] = part.content
|
|
510
392
|
end
|
|
511
393
|
end
|
|
512
394
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
# @return [String, nil] relationship type URI
|
|
64
|
+
def rel_type
|
|
65
|
+
@rel_type || definition&.rel_type
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [String, nil] MIME content type
|
|
69
|
+
def content_type
|
|
70
|
+
@content_type || definition&.content_type
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Package-relative path of the emitted part. Document-scoped
|
|
74
|
+
# parts live under word/; their target is already relative to
|
|
75
|
+
# word/.
|
|
76
|
+
#
|
|
77
|
+
# @return [String, nil] e.g. "word/charts/chart1.xml"
|
|
78
|
+
def path
|
|
79
|
+
target && "word/#{target}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Verbatim overrides carried from a loaded package.
|
|
83
|
+
attr_writer :rel_type, :content_type
|
|
84
|
+
|
|
85
|
+
# Key → reader lambdas backing hash-style access (kept as data
|
|
86
|
+
# so the dispatch stays declarative).
|
|
87
|
+
HASH_LOOKUP = {
|
|
88
|
+
r_id: lambda(&:r_id),
|
|
89
|
+
target: lambda(&:target),
|
|
90
|
+
rel_type: lambda(&:rel_type),
|
|
91
|
+
content_type: lambda(&:content_type),
|
|
92
|
+
content: lambda(&:content),
|
|
93
|
+
path: lambda(&:path),
|
|
94
|
+
}.freeze
|
|
95
|
+
|
|
96
|
+
# Hash-style read compatibility for former hash entries.
|
|
97
|
+
#
|
|
98
|
+
# @param key [Symbol, String] one of :r_id, :target, :rel_type,
|
|
99
|
+
# :content_type, :content, :path
|
|
100
|
+
# @return [Object, nil]
|
|
101
|
+
def [](key)
|
|
102
|
+
HASH_LOOKUP[key.to_sym]&.call(self)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|