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
|
@@ -12,6 +12,7 @@ module Uniword
|
|
|
12
12
|
autoload :FeatureFacade, "#{__dir__}/document_root/feature_facade"
|
|
13
13
|
|
|
14
14
|
include FeatureFacade
|
|
15
|
+
include DocumentStyling
|
|
15
16
|
include Uniword::DocumentInput
|
|
16
17
|
|
|
17
18
|
attribute :mc_ignorable, Uniword::Ooxml::Types::McIgnorable
|
|
@@ -94,28 +95,14 @@ module Uniword
|
|
|
94
95
|
# Additional attributes for DOCX metadata (not part of document.xml)
|
|
95
96
|
# These are stored in separate files within the DOCX package
|
|
96
97
|
attr_accessor :theme, :raw_html, :revisions, :comments
|
|
97
|
-
# Headers and footers (stored as hash: type => Header/Footer)
|
|
98
|
-
# Single-section only. For multi-section, use header_footer_parts.
|
|
99
|
-
attr_accessor :headers, :footers
|
|
100
|
-
# Multi-section headers/footers (ordered array):
|
|
101
|
-
# [{r_id: "rIdH1", target: "header1.xml", rel_type: "...",
|
|
102
|
-
# content_type: "...", content: Header|Footer}]
|
|
103
|
-
attr_accessor :header_footer_parts
|
|
104
98
|
# Footnotes and endnotes (separate XML parts in DOCX package)
|
|
105
99
|
attr_accessor :footnotes, :endnotes
|
|
106
|
-
# Image parts to embed in the DOCX package
|
|
107
|
-
# Hash: r_id => { path: String, data: String, content_type: String, target: String }
|
|
108
|
-
attr_accessor :image_parts
|
|
109
|
-
# Chart parts to embed in the DOCX package
|
|
110
|
-
# Hash: r_id => { xml: String, target: String }
|
|
111
|
-
attr_accessor :chart_parts
|
|
112
100
|
# Bibliography sources for sources.xml
|
|
113
101
|
attr_accessor :bibliography_sources
|
|
114
|
-
# OLE/embedded object binaries (word/embeddings/*)
|
|
115
|
-
attr_accessor :embeddings
|
|
116
102
|
# Round-trip parts (copied from DocxPackage during load)
|
|
117
103
|
attr_accessor :settings, :font_table, :web_settings, :document_rels, :theme_rels,
|
|
118
|
-
:package_rels, :content_types, :custom_properties, :custom_xml_items
|
|
104
|
+
:package_rels, :content_types, :custom_properties, :custom_xml_items,
|
|
105
|
+
:settings_rels, :footnotes_rels, :endnotes_rels
|
|
119
106
|
# Central ID allocator — preserves IDs across build/save cycle
|
|
120
107
|
attr_accessor :allocator
|
|
121
108
|
|
|
@@ -123,6 +110,100 @@ module Uniword
|
|
|
123
110
|
# (removing from attr_accessor to avoid shadowing custom getters)
|
|
124
111
|
attr_writer :app_properties, :core_properties, :bookmarks
|
|
125
112
|
|
|
113
|
+
# Unified header/footer part store — the single storage path
|
|
114
|
+
# for both round-tripped and builder-created headers/footers.
|
|
115
|
+
#
|
|
116
|
+
# @return [Docx::HeaderFooterPartCollection] ordered part store
|
|
117
|
+
def header_footer_parts
|
|
118
|
+
@header_footer_parts ||= Docx::HeaderFooterPartCollection.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Replace the whole store (accepts HeaderFooterPart objects and
|
|
122
|
+
# legacy part hashes).
|
|
123
|
+
def header_footer_parts=(parts)
|
|
124
|
+
header_footer_parts.replace_all(parts)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Headers view over the unified store (Hash-style by sectPr
|
|
128
|
+
# type, Array-style over parts).
|
|
129
|
+
#
|
|
130
|
+
# @return [Docx::HeaderFooterView]
|
|
131
|
+
def headers
|
|
132
|
+
@headers ||=
|
|
133
|
+
Docx::HeaderFooterView.new(header_footer_parts, :header)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Bulk-assign headers (nil clears; Hash of type => model; Array
|
|
137
|
+
# of models/parts).
|
|
138
|
+
def headers=(value)
|
|
139
|
+
headers.replace(value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Footers view over the unified store.
|
|
143
|
+
#
|
|
144
|
+
# @return [Docx::HeaderFooterView]
|
|
145
|
+
def footers
|
|
146
|
+
@footers ||=
|
|
147
|
+
Docx::HeaderFooterView.new(header_footer_parts, :footer)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Bulk-assign footers (see #headers=).
|
|
151
|
+
def footers=(value)
|
|
152
|
+
footers.replace(value)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Image parts (word/media/* binaries), keyed by rId.
|
|
156
|
+
#
|
|
157
|
+
# @return [Docx::PartCollection] rId => Docx::ImagePart
|
|
158
|
+
def image_parts
|
|
159
|
+
@image_parts ||= Docx::PartCollection.new(:r_id, Docx::ImagePart)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Bulk-assign image parts (Hash of rId => ImagePart/legacy part
|
|
163
|
+
# hash; nil clears).
|
|
164
|
+
def image_parts=(value)
|
|
165
|
+
image_parts.replace_all(value)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Chart parts to embed in the DOCX package, keyed by rId.
|
|
169
|
+
#
|
|
170
|
+
# @return [Docx::PartCollection] rId => ChartPart
|
|
171
|
+
def chart_parts
|
|
172
|
+
@chart_parts ||= Docx::PartCollection.new(:r_id, Docx::ChartPart)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Bulk-assign chart parts (Hash of rId => ChartPart/hash; nil clears).
|
|
176
|
+
def chart_parts=(value)
|
|
177
|
+
chart_parts.replace_all(value)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# OLE/embedded object binaries (word/embeddings/*), keyed by target.
|
|
181
|
+
#
|
|
182
|
+
# @return [Docx::PartCollection] target => Part
|
|
183
|
+
def embeddings
|
|
184
|
+
@embeddings ||= Docx::PartCollection.new(:target, Docx::Part)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Bulk-assign embeddings (Hash of target => Part/binary; nil clears).
|
|
188
|
+
def embeddings=(value)
|
|
189
|
+
embeddings.replace_all(value)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Raw passthrough parts (unmodelled package parts carried
|
|
193
|
+
# byte-for-byte), keyed by package path. Mirrored from the
|
|
194
|
+
# package on load and back on save via the registry-driven
|
|
195
|
+
# document↔package copies (:raw_parts definition).
|
|
196
|
+
#
|
|
197
|
+
# @return [Docx::PartCollection] package path => Docx::RawPart
|
|
198
|
+
def raw_parts
|
|
199
|
+
@raw_parts ||= Docx::PartCollection.new(:path, Docx::RawPart)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Bulk-assign raw parts (Hash of path => RawPart/hash; nil clears).
|
|
203
|
+
def raw_parts=(value)
|
|
204
|
+
raw_parts.replace_all(value)
|
|
205
|
+
end
|
|
206
|
+
|
|
126
207
|
# Get app_properties (lazy initialization)
|
|
127
208
|
#
|
|
128
209
|
# @return [Uniword::Ooxml::AppProperties] The app properties object
|
|
@@ -168,17 +249,24 @@ module Uniword
|
|
|
168
249
|
#
|
|
169
250
|
# @param path [String] Output file path
|
|
170
251
|
# @param format [Symbol] Format (:docx, :mhtml, :auto)
|
|
171
|
-
|
|
252
|
+
# @param profile [Docx::Profile, nil] Profile for reconciliation
|
|
253
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
254
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
255
|
+
# @return [void]
|
|
256
|
+
def save(path, format: :auto, profile: nil, validate: nil)
|
|
172
257
|
writer = DocumentWriter.new(self)
|
|
173
|
-
writer.save(path, format: format, profile: profile)
|
|
258
|
+
writer.save(path, format: format, profile: profile, validate: validate)
|
|
174
259
|
end
|
|
175
260
|
|
|
176
261
|
# Save document to DOCX file using DocxPackage
|
|
177
262
|
#
|
|
178
263
|
# @param path [String] Output file path
|
|
179
264
|
# @param profile [Docx::Profile, nil] Profile for reconciliation
|
|
180
|
-
|
|
181
|
-
|
|
265
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
266
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
267
|
+
# @return [void]
|
|
268
|
+
def to_file(path, profile: nil, validate: nil)
|
|
269
|
+
Docx::Package.to_file(self, path, profile: profile, validate: validate)
|
|
182
270
|
end
|
|
183
271
|
|
|
184
272
|
# Get all paragraph text
|
|
@@ -226,26 +314,26 @@ module Uniword
|
|
|
226
314
|
end
|
|
227
315
|
|
|
228
316
|
# Check if document structure is valid.
|
|
229
|
-
# Runs
|
|
317
|
+
# Runs model-level validation rules via Validation::Engine.
|
|
230
318
|
# Use the verify CLI command for full OPC + XSD + semantic validation.
|
|
231
319
|
#
|
|
232
320
|
# @return [Boolean] true if document has valid structure
|
|
233
321
|
def valid?
|
|
234
|
-
|
|
322
|
+
validation_errors.empty?
|
|
235
323
|
end
|
|
236
324
|
|
|
237
325
|
# Get structural validation errors.
|
|
238
326
|
#
|
|
239
327
|
# @return [Array<String>] Error messages
|
|
240
328
|
def validation_errors
|
|
241
|
-
|
|
329
|
+
structural_issues.select(&:error?).map(&:message)
|
|
242
330
|
end
|
|
243
331
|
|
|
244
332
|
# Get structural validation warnings.
|
|
245
333
|
#
|
|
246
334
|
# @return [Array<String>] Warning messages
|
|
247
335
|
def validation_warnings
|
|
248
|
-
|
|
336
|
+
structural_issues.reject(&:error?).map(&:message)
|
|
249
337
|
end
|
|
250
338
|
|
|
251
339
|
# Get bookmarks from document paragraphs
|
|
@@ -265,104 +353,6 @@ module Uniword
|
|
|
265
353
|
|
|
266
354
|
# Apply theme to document
|
|
267
355
|
#
|
|
268
|
-
# Applies a Uniword theme by name, updating doc defaults and
|
|
269
|
-
# built-in heading/hyperlink styles to reference the theme.
|
|
270
|
-
#
|
|
271
|
-
# @param name [String, Symbol] Theme slug (e.g., 'meridian', 'corporate')
|
|
272
|
-
# @param options [Hash] optional overrides
|
|
273
|
-
# @option options [Hash] :colors override specific color keys
|
|
274
|
-
# @option options [String] :major_font override major font
|
|
275
|
-
# @option options [String] :minor_font override minor font
|
|
276
|
-
# @return [self] For method chaining
|
|
277
|
-
def apply_theme(name, **options)
|
|
278
|
-
friendly = Themes::Theme.load(name.to_s)
|
|
279
|
-
|
|
280
|
-
options[:colors]&.each do |key, value|
|
|
281
|
-
friendly.color_scheme[key.to_s] = value
|
|
282
|
-
end
|
|
283
|
-
friendly.font_scheme.major_font = options[:major_font] if options[:major_font]
|
|
284
|
-
friendly.font_scheme.minor_font = options[:minor_font] if options[:minor_font]
|
|
285
|
-
|
|
286
|
-
word_theme = Themes::ThemeTransformation.new.to_word(friendly)
|
|
287
|
-
Themes::ThemeApplicator.new.apply(word_theme, self)
|
|
288
|
-
self
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
# Apply theme from .thmx file
|
|
292
|
-
#
|
|
293
|
-
# @param path [String] Path to .thmx file
|
|
294
|
-
# @param variant [String, Integer, nil] Optional variant
|
|
295
|
-
# @return [self] For method chaining
|
|
296
|
-
def apply_theme_file(path, variant: nil)
|
|
297
|
-
loader = Themes::ThemeLoader.new
|
|
298
|
-
self.theme = if variant
|
|
299
|
-
loader.load_with_variant(path, variant)
|
|
300
|
-
else
|
|
301
|
-
loader.load(path)
|
|
302
|
-
end
|
|
303
|
-
self
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# Apply StyleSet to document
|
|
307
|
-
#
|
|
308
|
-
# @param name [String, Symbol] StyleSet slug (e.g., 'signature', 'heritage')
|
|
309
|
-
# @param strategy [Symbol] Application strategy (:keep_existing, :replace, :rename)
|
|
310
|
-
# @return [self] For method chaining
|
|
311
|
-
def apply_styleset(name, strategy: :keep_existing)
|
|
312
|
-
styleset = Uniword::Stylesets::YamlStyleSetLoader.load_bundled(name.to_s)
|
|
313
|
-
styleset.apply_to(self, strategy: strategy)
|
|
314
|
-
self
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Auto-transition from MS theme to Uniword equivalent
|
|
318
|
-
#
|
|
319
|
-
# Detects the MS theme in the document's embedded theme and replaces
|
|
320
|
-
# it with the corresponding Uniword theme (font-substituted, renamed).
|
|
321
|
-
#
|
|
322
|
-
# @return [Hash, nil] { uniword_slug:, ms_name: } or nil if no match
|
|
323
|
-
#
|
|
324
|
-
# @example
|
|
325
|
-
# result = doc.auto_transition_theme
|
|
326
|
-
# puts "Transitioned from #{result[:ms_name]} to #{result[:uniword_slug]}"
|
|
327
|
-
def auto_transition_theme
|
|
328
|
-
Resource::ThemeTransition.auto_transition!(self)
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Apply theme from another document
|
|
332
|
-
#
|
|
333
|
-
# @param source_path [String] Path to source .docx file
|
|
334
|
-
# @return [self] For method chaining
|
|
335
|
-
def apply_theme_from(source_path)
|
|
336
|
-
source_doc = Uniword.load(source_path)
|
|
337
|
-
self.theme = source_doc.theme.dup if source_doc.theme
|
|
338
|
-
self
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
# Apply styles from another document
|
|
342
|
-
#
|
|
343
|
-
# @param source_path [String] Path to source .docx file
|
|
344
|
-
# @param strategy [Symbol] Conflict resolution strategy
|
|
345
|
-
# @return [self] For method chaining
|
|
346
|
-
def apply_styles_from(source_path, strategy: :keep_existing)
|
|
347
|
-
source_doc = Uniword.load(source_path)
|
|
348
|
-
styles_configuration.merge(source_doc.styles_configuration,
|
|
349
|
-
conflict_resolution: strategy)
|
|
350
|
-
self
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Apply both theme and styles from a template document
|
|
354
|
-
#
|
|
355
|
-
# @param template_path [String] Path to template .docx file
|
|
356
|
-
# @param strategy [Symbol] Conflict resolution strategy for styles
|
|
357
|
-
# @return [self] For method chaining
|
|
358
|
-
def apply_template(template_path, strategy: :keep_existing)
|
|
359
|
-
template_doc = Uniword.load(template_path)
|
|
360
|
-
self.theme = template_doc.theme.dup if template_doc.theme
|
|
361
|
-
styles_configuration.merge(template_doc.styles_configuration,
|
|
362
|
-
conflict_resolution: strategy)
|
|
363
|
-
self
|
|
364
|
-
end
|
|
365
|
-
|
|
366
356
|
# Custom inspect for readable output
|
|
367
357
|
#
|
|
368
358
|
# @return [String] Human-readable representation
|
|
@@ -376,6 +366,15 @@ module Uniword
|
|
|
376
366
|
def to_html_document
|
|
377
367
|
Uniword::Transformation::OoxmlToHtmlConverter.document_to_html(self)
|
|
378
368
|
end
|
|
369
|
+
|
|
370
|
+
private
|
|
371
|
+
|
|
372
|
+
# Run model-level validation rules against this document.
|
|
373
|
+
#
|
|
374
|
+
# @return [Array<Validation::Report::ValidationIssue>] issues found
|
|
375
|
+
def structural_issues
|
|
376
|
+
Validation::Engine.run(Validation::Rules::ModelContext.new(self))
|
|
377
|
+
end
|
|
379
378
|
end
|
|
380
379
|
end
|
|
381
380
|
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Wordprocessingml
|
|
5
|
+
# Theme and style application for DocumentRoot — Word's
|
|
6
|
+
# Design/Layout/Styles surfaces as document-level API.
|
|
7
|
+
#
|
|
8
|
+
# Extracted from DocumentRoot to keep the document model focused:
|
|
9
|
+
# bundled and file-based theme application, stylesets, font and
|
|
10
|
+
# color schemes, font replacement, page setup, and style
|
|
11
|
+
# management (rename/remove).
|
|
12
|
+
module DocumentStyling
|
|
13
|
+
# Applies a Uniword theme by name, updating doc defaults and
|
|
14
|
+
# built-in heading/hyperlink styles to reference the theme.
|
|
15
|
+
#
|
|
16
|
+
# @param name [String, Symbol] Theme slug (e.g., 'meridian',
|
|
17
|
+
# 'corporate')
|
|
18
|
+
# @param options [Hash] optional overrides
|
|
19
|
+
# @option options [Hash] :colors override specific color keys
|
|
20
|
+
# @option options [String] :major_font override major font
|
|
21
|
+
# @option options [String] :minor_font override minor font
|
|
22
|
+
# @return [self] For method chaining
|
|
23
|
+
def apply_theme(name, **options)
|
|
24
|
+
friendly = Themes::Theme.load(name.to_s)
|
|
25
|
+
|
|
26
|
+
options[:colors]&.each do |key, value|
|
|
27
|
+
friendly.color_scheme[key.to_s] = value
|
|
28
|
+
end
|
|
29
|
+
if options[:major_font]
|
|
30
|
+
friendly.font_scheme.major_font = options[:major_font]
|
|
31
|
+
end
|
|
32
|
+
if options[:minor_font]
|
|
33
|
+
friendly.font_scheme.minor_font = options[:minor_font]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
word_theme = Themes::ThemeTransformation.new.to_word(friendly)
|
|
37
|
+
Themes::ThemeApplicator.new.apply(word_theme, self)
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Apply theme from .thmx file
|
|
42
|
+
#
|
|
43
|
+
# @param path [String] Path to .thmx file
|
|
44
|
+
# @param variant [String, Integer, nil] Optional variant
|
|
45
|
+
# @return [self] For method chaining
|
|
46
|
+
def apply_theme_file(path, variant: nil)
|
|
47
|
+
loader = Themes::ThemeLoader.new
|
|
48
|
+
self.theme = if variant
|
|
49
|
+
loader.load_with_variant(path, variant)
|
|
50
|
+
else
|
|
51
|
+
loader.load(path)
|
|
52
|
+
end
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Apply StyleSet to document
|
|
57
|
+
#
|
|
58
|
+
# @param name [String, Symbol] StyleSet slug (e.g., 'signature',
|
|
59
|
+
# 'heritage')
|
|
60
|
+
# @param strategy [Symbol] Application strategy (:keep_existing,
|
|
61
|
+
# :replace, :rename)
|
|
62
|
+
# @return [self] For method chaining
|
|
63
|
+
def apply_styleset(name, strategy: :keep_existing)
|
|
64
|
+
styleset = Uniword::Stylesets::YamlStyleSetLoader
|
|
65
|
+
.load_bundled(name.to_s)
|
|
66
|
+
styleset.apply_to(self, strategy: strategy)
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Apply a bundled font scheme to the document's theme
|
|
71
|
+
#
|
|
72
|
+
# Mirrors Word's Design → Fonts gallery: replaces only the theme's
|
|
73
|
+
# fontScheme — colors and formats are untouched. Creates the theme
|
|
74
|
+
# part from the bundled Office theme when the document has none.
|
|
75
|
+
#
|
|
76
|
+
# @param name [String, Symbol] Font scheme slug (data/font_schemes/)
|
|
77
|
+
# @return [self] For method chaining
|
|
78
|
+
# @raise [ArgumentError] if the scheme is unknown
|
|
79
|
+
def apply_font_scheme(name)
|
|
80
|
+
fonts = Resource::FontSchemeLoader.load(name.to_s)
|
|
81
|
+
transformation = Themes::ThemeTransformation.new
|
|
82
|
+
ensure_theme!(transformation)
|
|
83
|
+
theme.theme_elements.font_scheme =
|
|
84
|
+
transformation.build_font_scheme(fonts)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Apply a bundled color scheme to the document's theme
|
|
89
|
+
#
|
|
90
|
+
# Mirrors Word's Design → Colors gallery: replaces only the theme's
|
|
91
|
+
# clrScheme — fonts and formats are untouched. Creates the theme
|
|
92
|
+
# part from the bundled Office theme when the document has none.
|
|
93
|
+
#
|
|
94
|
+
# @param name [String, Symbol] Color scheme slug (data/color_schemes/)
|
|
95
|
+
# @return [self] For method chaining
|
|
96
|
+
# @raise [ArgumentError] if the scheme is unknown
|
|
97
|
+
def apply_color_scheme(name)
|
|
98
|
+
colors = Resource::ColorSchemeLoader.load(name.to_s)
|
|
99
|
+
transformation = Themes::ThemeTransformation.new
|
|
100
|
+
ensure_theme!(transformation)
|
|
101
|
+
theme.theme_elements.clr_scheme =
|
|
102
|
+
transformation.build_color_scheme(colors)
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Replace one font family with another across the document
|
|
107
|
+
#
|
|
108
|
+
# Mirrors Word's Home → Replace → Replace Fonts: rewrites rFonts
|
|
109
|
+
# references in styles and defaults, body content, headers,
|
|
110
|
+
# footers, notes, comments, and numbering. Theme font references
|
|
111
|
+
# (asciiTheme etc.) are untouched — use #apply_font_scheme for
|
|
112
|
+
# those.
|
|
113
|
+
#
|
|
114
|
+
# @param from [String] Font family to replace (exact match)
|
|
115
|
+
# @param to [String] Replacement font family
|
|
116
|
+
# @return [Integer] Number of rFonts attribute values rewritten
|
|
117
|
+
def replace_font(from:, to:)
|
|
118
|
+
replacer = FontReplacer.new(from: from, to: to)
|
|
119
|
+
replacer.replace(self)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Apply uniform page setup to every section of the document
|
|
123
|
+
#
|
|
124
|
+
# Mirrors Word's Layout dialog: named paper sizes, orientation
|
|
125
|
+
# (with Word-style dimension swap), and margins.
|
|
126
|
+
#
|
|
127
|
+
# @param size [String, nil] Paper size: letter, legal, a4, a5,
|
|
128
|
+
# executive
|
|
129
|
+
# @param orientation [String, nil] "portrait" or "landscape"
|
|
130
|
+
# @param margins [Integer, String, nil] Uniform margin for all
|
|
131
|
+
# sides (twips, or "1in" / "2.5cm" / "25mm")
|
|
132
|
+
# @param top, right, bottom, left [Integer, String, nil] Per-side
|
|
133
|
+
# margin overrides
|
|
134
|
+
# @return [Integer] Number of sections updated
|
|
135
|
+
def apply_page_setup(size: nil, orientation: nil, margins: nil,
|
|
136
|
+
top: nil, right: nil, bottom: nil, left: nil)
|
|
137
|
+
setup = PageSetup.new(size: size, orientation: orientation,
|
|
138
|
+
margins: margins, top: top, right: right,
|
|
139
|
+
bottom: bottom, left: left)
|
|
140
|
+
setup.apply(self)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Remove one style from the document's style definitions
|
|
144
|
+
#
|
|
145
|
+
# Default styles (w:default="1") are never removed.
|
|
146
|
+
#
|
|
147
|
+
# @param style_id [String] Style id (w:styleId)
|
|
148
|
+
# @return [Style, nil] The removed style, or nil when not
|
|
149
|
+
# removed (absent or protected)
|
|
150
|
+
def remove_style(style_id)
|
|
151
|
+
style = styles_configuration.style(style_id)
|
|
152
|
+
return unless StyleCleanup.new(self).remove?(style_id)
|
|
153
|
+
|
|
154
|
+
style
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Rename a style's display name (w:name) — Word's style rename.
|
|
158
|
+
# References (pStyle/rStyle/tblStyle) target the styleId, so they
|
|
159
|
+
# keep working unchanged.
|
|
160
|
+
#
|
|
161
|
+
# @param identifier [String] Style id (w:styleId) or current
|
|
162
|
+
# display name (w:name)
|
|
163
|
+
# @param new_name [String] New display name
|
|
164
|
+
# @return [Style, nil] The renamed style, or nil when not found
|
|
165
|
+
def rename_style(identifier, new_name)
|
|
166
|
+
style = styles_configuration.style(identifier)
|
|
167
|
+
return unless style
|
|
168
|
+
|
|
169
|
+
style.name = StyleName.new(val: new_name)
|
|
170
|
+
style
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Remove every style that no content references — Word's
|
|
174
|
+
# Styles-pane decluttering as one call. Default styles are kept.
|
|
175
|
+
#
|
|
176
|
+
# @return [Array<String>] Ids of removed styles
|
|
177
|
+
def remove_unused_styles
|
|
178
|
+
StyleCleanup.new(self).remove_unused
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Auto-transition from MS theme to Uniword equivalent
|
|
182
|
+
#
|
|
183
|
+
# Detects the MS theme in the document's embedded theme and replaces
|
|
184
|
+
# it with the corresponding Uniword theme (font-substituted, renamed).
|
|
185
|
+
#
|
|
186
|
+
# @return [Hash, nil] { uniword_slug:, ms_name: } or nil if no match
|
|
187
|
+
#
|
|
188
|
+
# @example
|
|
189
|
+
# result = doc.auto_transition_theme
|
|
190
|
+
# puts "Transitioned from " \
|
|
191
|
+
# "#{result[:ms_name]} to #{result[:uniword_slug]}"
|
|
192
|
+
def auto_transition_theme
|
|
193
|
+
Resource::ThemeTransition.auto_transition!(self)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Apply theme from another document
|
|
197
|
+
#
|
|
198
|
+
# @param source_path [String] Path to source .docx file
|
|
199
|
+
# @return [self] For method chaining
|
|
200
|
+
def apply_theme_from(source_path)
|
|
201
|
+
source_doc = Uniword.load(source_path)
|
|
202
|
+
self.theme = source_doc.theme.dup if source_doc.theme
|
|
203
|
+
self
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Apply styles from another document
|
|
207
|
+
#
|
|
208
|
+
# @param source_path [String] Path to source .docx file
|
|
209
|
+
# @param strategy [Symbol] Conflict resolution strategy
|
|
210
|
+
# @return [self] For method chaining
|
|
211
|
+
def apply_styles_from(source_path, strategy: :keep_existing)
|
|
212
|
+
source_doc = Uniword.load(source_path)
|
|
213
|
+
styles_configuration.merge(source_doc.styles_configuration,
|
|
214
|
+
conflict_resolution: strategy)
|
|
215
|
+
self
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Apply both theme and styles from a template document
|
|
219
|
+
#
|
|
220
|
+
# @param template_path [String] Path to template .docx file
|
|
221
|
+
# @param strategy [Symbol] Conflict resolution strategy for styles
|
|
222
|
+
# @return [self] For method chaining
|
|
223
|
+
def apply_template(template_path, strategy: :keep_existing)
|
|
224
|
+
template_doc = Uniword.load(template_path)
|
|
225
|
+
self.theme = template_doc.theme.dup if template_doc.theme
|
|
226
|
+
styles_configuration.merge(template_doc.styles_configuration,
|
|
227
|
+
conflict_resolution: strategy)
|
|
228
|
+
self
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
# Ensure the document carries a theme part, creating it from a
|
|
234
|
+
# fresh parse of the bundled Office theme when absent (every
|
|
235
|
+
# document gets its own copy — no shared state).
|
|
236
|
+
#
|
|
237
|
+
# @param transformation [Themes::ThemeTransformation] Converter
|
|
238
|
+
# @return [void]
|
|
239
|
+
def ensure_theme!(transformation)
|
|
240
|
+
return if theme&.theme_elements
|
|
241
|
+
|
|
242
|
+
self.theme = transformation.default_office_theme
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -32,8 +32,11 @@ module Uniword
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
missing = endnote_entries.size - counts["endnote"]
|
|
35
|
+
return unless missing.positive?
|
|
36
|
+
|
|
37
|
+
order = Ooxml::ElementOrder.mutable_order(self)
|
|
35
38
|
missing.times do
|
|
36
|
-
|
|
39
|
+
order << Lutaml::Xml::Element.new("Element", "endnote")
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
|