uniword 1.2.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +223 -0
- data/CONTRIBUTING.md +1 -1
- data/config/ooxml/schemas/shared_types.yml +5 -4
- data/data/schemas/ecma/dc.xsd +118 -0
- data/data/schemas/ecma/dcmitype.xsd +50 -0
- data/data/schemas/ecma/dcterms.xsd +331 -0
- data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
- data/data/schemas/ecma/xml.xsd +117 -0
- data/lib/uniword/builder/bibliography_builder.rb +1 -1
- data/lib/uniword/builder/chart_builder.rb +19 -9
- data/lib/uniword/builder/comment_anchorer.rb +221 -0
- data/lib/uniword/builder/document_builder.rb +97 -6
- data/lib/uniword/builder/image_builder.rb +6 -5
- data/lib/uniword/builder/run_utils.rb +2 -0
- data/lib/uniword/builder.rb +1 -0
- data/lib/uniword/cli/fonts_cli.rb +52 -0
- data/lib/uniword/cli/main.rb +110 -11
- data/lib/uniword/cli/page_cli.rb +85 -0
- data/lib/uniword/cli/styles_cli.rb +204 -0
- data/lib/uniword/cli/theme_cli.rb +88 -0
- data/lib/uniword/cli/toc_cli.rb +22 -2
- data/lib/uniword/comment.rb +20 -15
- data/lib/uniword/comment_range.rb +3 -3
- data/lib/uniword/comments_part.rb +51 -19
- data/lib/uniword/configuration/configuration_loader.rb +5 -3
- data/lib/uniword/configuration.rb +93 -1
- data/lib/uniword/content_types.rb +19 -39
- data/lib/uniword/document_factory.rb +9 -1
- data/lib/uniword/document_writer.rb +16 -5
- data/lib/uniword/docx/chart_part.rb +34 -0
- data/lib/uniword/docx/custom_xml_item.rb +83 -0
- data/lib/uniword/docx/header_footer_part.rb +140 -0
- data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
- data/lib/uniword/docx/header_footer_view.rb +225 -0
- data/lib/uniword/docx/id_allocator.rb +130 -36
- data/lib/uniword/docx/package.rb +223 -35
- data/lib/uniword/docx/package_defaults.rb +58 -78
- data/lib/uniword/docx/package_integrity_checker.rb +312 -0
- data/lib/uniword/docx/package_serialization.rb +103 -231
- data/lib/uniword/docx/part.rb +106 -0
- data/lib/uniword/docx/part_collection.rb +123 -0
- data/lib/uniword/docx/reconciler/body.rb +59 -68
- data/lib/uniword/docx/reconciler/fix.rb +46 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
- data/lib/uniword/docx/reconciler/helpers.rb +9 -1
- data/lib/uniword/docx/reconciler/notes.rb +17 -8
- data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
- data/lib/uniword/docx/reconciler/parts.rb +36 -13
- data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
- data/lib/uniword/docx/reconciler/tables.rb +78 -16
- data/lib/uniword/docx/reconciler/theme.rb +7 -4
- data/lib/uniword/docx/reconciler.rb +36 -14
- data/lib/uniword/docx.rb +10 -0
- data/lib/uniword/drawingml/blip.rb +0 -1
- data/lib/uniword/drawingml/color_scheme.rb +5 -6
- data/lib/uniword/drawingml/font_scheme.rb +4 -8
- data/lib/uniword/drawingml/theme.rb +12 -6
- data/lib/uniword/errors.rb +8 -1
- data/lib/uniword/footer.rb +4 -0
- data/lib/uniword/header.rb +4 -0
- data/lib/uniword/hyperlink.rb +1 -1
- data/lib/uniword/image.rb +6 -6
- data/lib/uniword/lazy_loader.rb +1 -2
- data/lib/uniword/model_attribute_access.rb +20 -4
- data/lib/uniword/ooxml/dotx_package.rb +42 -2
- data/lib/uniword/ooxml/part_definition.rb +171 -0
- data/lib/uniword/ooxml/part_registry.rb +311 -0
- data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
- data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
- data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
- data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
- data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
- data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
- data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
- data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
- data/lib/uniword/ooxml/types.rb +10 -0
- data/lib/uniword/ooxml.rb +2 -0
- data/lib/uniword/properties/alignment.rb +8 -3
- data/lib/uniword/properties/border.rb +52 -6
- data/lib/uniword/properties/cell_vertical_align.rb +4 -1
- data/lib/uniword/properties/color_value.rb +8 -3
- data/lib/uniword/properties/highlight.rb +9 -5
- data/lib/uniword/properties/shading.rb +4 -3
- data/lib/uniword/properties/tab_stop.rb +6 -1
- data/lib/uniword/properties/table_justification.rb +4 -1
- data/lib/uniword/properties/underline.rb +4 -2
- data/lib/uniword/properties/vertical_align.rb +6 -3
- data/lib/uniword/review/review_manager.rb +23 -9
- data/lib/uniword/revision.rb +9 -9
- data/lib/uniword/schema/model_generator.rb +31 -15
- data/lib/uniword/shared_types/hex_color.rb +4 -1
- data/lib/uniword/shared_types/pixel_measure.rb +4 -1
- data/lib/uniword/shared_types/point_measure.rb +4 -1
- data/lib/uniword/shared_types/text_alignment.rb +6 -1
- data/lib/uniword/shared_types/twips_measure.rb +4 -1
- data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
- data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
- data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
- data/lib/uniword/template/variable_resolver.rb +40 -7
- data/lib/uniword/themes/theme_transformation.rb +55 -41
- data/lib/uniword/toc/toc_generator.rb +2 -0
- data/lib/uniword/validation/engine.rb +35 -0
- data/lib/uniword/validation/link_checker.rb +1 -1
- data/lib/uniword/validation/opc_validator.rb +0 -1
- data/lib/uniword/validation/report/layer_result.rb +0 -1
- data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
- data/lib/uniword/validation/report/verification_report.rb +0 -2
- data/lib/uniword/validation/report.rb +20 -0
- data/lib/uniword/validation/rules/base.rb +10 -2
- data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
- data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
- data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
- data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
- data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
- data/lib/uniword/validation/rules/document_context.rb +7 -0
- data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
- data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
- data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
- data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
- data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
- data/lib/uniword/validation/rules/images_rule.rb +0 -2
- data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/model_context.rb +32 -0
- data/lib/uniword/validation/rules/model_rule.rb +34 -0
- data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
- data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
- data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
- data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
- data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
- data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
- data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
- data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
- data/lib/uniword/validation/rules/tables_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_rule.rb +0 -2
- data/lib/uniword/validation/rules.rb +61 -23
- data/lib/uniword/validation/schema_registry.rb +11 -0
- data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
- data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
- data/lib/uniword/validation/validators.rb +0 -9
- data/lib/uniword/validation/verify_orchestrator.rb +0 -7
- data/lib/uniword/validation.rb +7 -2
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/vml/imagedata.rb +0 -1
- data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
- data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +211 -21
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
- data/lib/uniword/wordprocessingml/level.rb +6 -20
- data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
- data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
- data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
- data/lib/uniword/wordprocessingml/rsids.rb +0 -2
- data/lib/uniword/wordprocessingml/run.rb +3 -0
- data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
- data/lib/uniword/wordprocessingml/settings.rb +8 -34
- data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/style.rb +2 -1
- data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
- data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
- data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
- data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
- data/lib/uniword/wordprocessingml.rb +4 -0
- data/lib/uniword.rb +27 -2
- metadata +38 -19
- data/config/validation_rules.yml +0 -60
- data/config/warning_rules.yml +0 -57
- data/lib/uniword/validation/document_validator.rb +0 -272
- data/lib/uniword/validation/structural_validator.rb +0 -116
- data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
- data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
- data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
- data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
- data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
- data/lib/uniword/validators/element_validator.rb +0 -93
- data/lib/uniword/validators/paragraph_validator.rb +0 -116
- data/lib/uniword/validators/table_validator.rb +0 -134
- data/lib/uniword/validators.rb +0 -9
- data/lib/uniword/warnings/warning.rb +0 -130
- data/lib/uniword/warnings/warning_collector.rb +0 -234
- data/lib/uniword/warnings/warning_report.rb +0 -159
- data/lib/uniword/warnings.rb +0 -9
|
@@ -94,28 +94,17 @@ module Uniword
|
|
|
94
94
|
# Additional attributes for DOCX metadata (not part of document.xml)
|
|
95
95
|
# These are stored in separate files within the DOCX package
|
|
96
96
|
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
97
|
# Footnotes and endnotes (separate XML parts in DOCX package)
|
|
105
98
|
attr_accessor :footnotes, :endnotes
|
|
106
99
|
# Image parts to embed in the DOCX package
|
|
107
100
|
# Hash: r_id => { path: String, data: String, content_type: String, target: String }
|
|
108
101
|
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
102
|
# Bibliography sources for sources.xml
|
|
113
103
|
attr_accessor :bibliography_sources
|
|
114
|
-
# OLE/embedded object binaries (word/embeddings/*)
|
|
115
|
-
attr_accessor :embeddings
|
|
116
104
|
# Round-trip parts (copied from DocxPackage during load)
|
|
117
105
|
attr_accessor :settings, :font_table, :web_settings, :document_rels, :theme_rels,
|
|
118
|
-
:package_rels, :content_types, :custom_properties, :custom_xml_items
|
|
106
|
+
:package_rels, :content_types, :custom_properties, :custom_xml_items,
|
|
107
|
+
:settings_rels, :footnotes_rels, :endnotes_rels
|
|
119
108
|
# Central ID allocator — preserves IDs across build/save cycle
|
|
120
109
|
attr_accessor :allocator
|
|
121
110
|
|
|
@@ -123,6 +112,72 @@ module Uniword
|
|
|
123
112
|
# (removing from attr_accessor to avoid shadowing custom getters)
|
|
124
113
|
attr_writer :app_properties, :core_properties, :bookmarks
|
|
125
114
|
|
|
115
|
+
# Unified header/footer part store — the single storage path
|
|
116
|
+
# for both round-tripped and builder-created headers/footers.
|
|
117
|
+
#
|
|
118
|
+
# @return [Docx::HeaderFooterPartCollection] ordered part store
|
|
119
|
+
def header_footer_parts
|
|
120
|
+
@header_footer_parts ||= Docx::HeaderFooterPartCollection.new
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Replace the whole store (accepts HeaderFooterPart objects and
|
|
124
|
+
# legacy part hashes).
|
|
125
|
+
def header_footer_parts=(parts)
|
|
126
|
+
header_footer_parts.replace_all(parts)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Headers view over the unified store (Hash-style by sectPr
|
|
130
|
+
# type, Array-style over parts).
|
|
131
|
+
#
|
|
132
|
+
# @return [Docx::HeaderFooterView]
|
|
133
|
+
def headers
|
|
134
|
+
@headers ||=
|
|
135
|
+
Docx::HeaderFooterView.new(header_footer_parts, :header)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Bulk-assign headers (nil clears; Hash of type => model; Array
|
|
139
|
+
# of models/parts).
|
|
140
|
+
def headers=(value)
|
|
141
|
+
headers.replace(value)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Footers view over the unified store.
|
|
145
|
+
#
|
|
146
|
+
# @return [Docx::HeaderFooterView]
|
|
147
|
+
def footers
|
|
148
|
+
@footers ||=
|
|
149
|
+
Docx::HeaderFooterView.new(header_footer_parts, :footer)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Bulk-assign footers (see #headers=).
|
|
153
|
+
def footers=(value)
|
|
154
|
+
footers.replace(value)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Chart parts to embed in the DOCX package, keyed by rId.
|
|
158
|
+
#
|
|
159
|
+
# @return [Docx::PartCollection] rId => ChartPart
|
|
160
|
+
def chart_parts
|
|
161
|
+
@chart_parts ||= Docx::PartCollection.new(:r_id, Docx::ChartPart)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Bulk-assign chart parts (Hash of rId => ChartPart/hash; nil clears).
|
|
165
|
+
def chart_parts=(value)
|
|
166
|
+
chart_parts.replace_all(value)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# OLE/embedded object binaries (word/embeddings/*), keyed by target.
|
|
170
|
+
#
|
|
171
|
+
# @return [Docx::PartCollection] target => Part
|
|
172
|
+
def embeddings
|
|
173
|
+
@embeddings ||= Docx::PartCollection.new(:target, Docx::Part)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Bulk-assign embeddings (Hash of target => Part/binary; nil clears).
|
|
177
|
+
def embeddings=(value)
|
|
178
|
+
embeddings.replace_all(value)
|
|
179
|
+
end
|
|
180
|
+
|
|
126
181
|
# Get app_properties (lazy initialization)
|
|
127
182
|
#
|
|
128
183
|
# @return [Uniword::Ooxml::AppProperties] The app properties object
|
|
@@ -168,17 +223,24 @@ module Uniword
|
|
|
168
223
|
#
|
|
169
224
|
# @param path [String] Output file path
|
|
170
225
|
# @param format [Symbol] Format (:docx, :mhtml, :auto)
|
|
171
|
-
|
|
226
|
+
# @param profile [Docx::Profile, nil] Profile for reconciliation
|
|
227
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
228
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
229
|
+
# @return [void]
|
|
230
|
+
def save(path, format: :auto, profile: nil, validate: nil)
|
|
172
231
|
writer = DocumentWriter.new(self)
|
|
173
|
-
writer.save(path, format: format, profile: profile)
|
|
232
|
+
writer.save(path, format: format, profile: profile, validate: validate)
|
|
174
233
|
end
|
|
175
234
|
|
|
176
235
|
# Save document to DOCX file using DocxPackage
|
|
177
236
|
#
|
|
178
237
|
# @param path [String] Output file path
|
|
179
238
|
# @param profile [Docx::Profile, nil] Profile for reconciliation
|
|
180
|
-
|
|
181
|
-
|
|
239
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
240
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
241
|
+
# @return [void]
|
|
242
|
+
def to_file(path, profile: nil, validate: nil)
|
|
243
|
+
Docx::Package.to_file(self, path, profile: profile, validate: validate)
|
|
182
244
|
end
|
|
183
245
|
|
|
184
246
|
# Get all paragraph text
|
|
@@ -226,26 +288,26 @@ module Uniword
|
|
|
226
288
|
end
|
|
227
289
|
|
|
228
290
|
# Check if document structure is valid.
|
|
229
|
-
# Runs
|
|
291
|
+
# Runs model-level validation rules via Validation::Engine.
|
|
230
292
|
# Use the verify CLI command for full OPC + XSD + semantic validation.
|
|
231
293
|
#
|
|
232
294
|
# @return [Boolean] true if document has valid structure
|
|
233
295
|
def valid?
|
|
234
|
-
|
|
296
|
+
validation_errors.empty?
|
|
235
297
|
end
|
|
236
298
|
|
|
237
299
|
# Get structural validation errors.
|
|
238
300
|
#
|
|
239
301
|
# @return [Array<String>] Error messages
|
|
240
302
|
def validation_errors
|
|
241
|
-
|
|
303
|
+
structural_issues.select(&:error?).map(&:message)
|
|
242
304
|
end
|
|
243
305
|
|
|
244
306
|
# Get structural validation warnings.
|
|
245
307
|
#
|
|
246
308
|
# @return [Array<String>] Warning messages
|
|
247
309
|
def validation_warnings
|
|
248
|
-
|
|
310
|
+
structural_issues.reject(&:error?).map(&:message)
|
|
249
311
|
end
|
|
250
312
|
|
|
251
313
|
# Get bookmarks from document paragraphs
|
|
@@ -314,6 +376,113 @@ module Uniword
|
|
|
314
376
|
self
|
|
315
377
|
end
|
|
316
378
|
|
|
379
|
+
# Apply a bundled font scheme to the document's theme
|
|
380
|
+
#
|
|
381
|
+
# Mirrors Word's Design → Fonts gallery: replaces only the theme's
|
|
382
|
+
# fontScheme — colors and formats are untouched. Creates the theme
|
|
383
|
+
# part from the bundled Office theme when the document has none.
|
|
384
|
+
#
|
|
385
|
+
# @param name [String, Symbol] Font scheme slug (data/font_schemes/)
|
|
386
|
+
# @return [self] For method chaining
|
|
387
|
+
# @raise [ArgumentError] if the scheme is unknown
|
|
388
|
+
def apply_font_scheme(name)
|
|
389
|
+
fonts = Resource::FontSchemeLoader.load(name.to_s)
|
|
390
|
+
transformation = Themes::ThemeTransformation.new
|
|
391
|
+
ensure_theme!(transformation)
|
|
392
|
+
theme.theme_elements.font_scheme =
|
|
393
|
+
transformation.build_font_scheme(fonts)
|
|
394
|
+
self
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Apply a bundled color scheme to the document's theme
|
|
398
|
+
#
|
|
399
|
+
# Mirrors Word's Design → Colors gallery: replaces only the theme's
|
|
400
|
+
# clrScheme — fonts and formats are untouched. Creates the theme
|
|
401
|
+
# part from the bundled Office theme when the document has none.
|
|
402
|
+
#
|
|
403
|
+
# @param name [String, Symbol] Color scheme slug (data/color_schemes/)
|
|
404
|
+
# @return [self] For method chaining
|
|
405
|
+
# @raise [ArgumentError] if the scheme is unknown
|
|
406
|
+
def apply_color_scheme(name)
|
|
407
|
+
colors = Resource::ColorSchemeLoader.load(name.to_s)
|
|
408
|
+
transformation = Themes::ThemeTransformation.new
|
|
409
|
+
ensure_theme!(transformation)
|
|
410
|
+
theme.theme_elements.clr_scheme =
|
|
411
|
+
transformation.build_color_scheme(colors)
|
|
412
|
+
self
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Replace one font family with another across the document
|
|
416
|
+
#
|
|
417
|
+
# Mirrors Word's Home → Replace → Replace Fonts: rewrites rFonts
|
|
418
|
+
# references in styles and defaults, body content, headers,
|
|
419
|
+
# footers, notes, comments, and numbering. Theme font references
|
|
420
|
+
# (asciiTheme etc.) are untouched — use #apply_font_scheme for
|
|
421
|
+
# those.
|
|
422
|
+
#
|
|
423
|
+
# @param from [String] Font family to replace (exact match)
|
|
424
|
+
# @param to [String] Replacement font family
|
|
425
|
+
# @return [Integer] Number of rFonts attribute values rewritten
|
|
426
|
+
def replace_font(from:, to:)
|
|
427
|
+
replacer = FontReplacer.new(from: from, to: to)
|
|
428
|
+
replacer.replace(self)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Apply uniform page setup to every section of the document
|
|
432
|
+
#
|
|
433
|
+
# Mirrors Word's Layout dialog: named paper sizes, orientation
|
|
434
|
+
# (with Word-style dimension swap), and margins.
|
|
435
|
+
#
|
|
436
|
+
# @param size [String, nil] Paper size: letter, legal, a4, a5,
|
|
437
|
+
# executive
|
|
438
|
+
# @param orientation [String, nil] "portrait" or "landscape"
|
|
439
|
+
# @param margins [Integer, String, nil] Uniform margin for all
|
|
440
|
+
# sides (twips, or "1in" / "2.5cm" / "25mm")
|
|
441
|
+
# @param top, right, bottom, left [Integer, String, nil] Per-side
|
|
442
|
+
# margin overrides
|
|
443
|
+
# @return [Integer] Number of sections updated
|
|
444
|
+
def apply_page_setup(size: nil, orientation: nil, margins: nil,
|
|
445
|
+
top: nil, right: nil, bottom: nil, left: nil)
|
|
446
|
+
setup = PageSetup.new(size: size, orientation: orientation,
|
|
447
|
+
margins: margins, top: top, right: right,
|
|
448
|
+
bottom: bottom, left: left)
|
|
449
|
+
setup.apply(self)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Remove one style from the document's style definitions
|
|
453
|
+
#
|
|
454
|
+
# Default styles (w:default="1") are never removed.
|
|
455
|
+
#
|
|
456
|
+
# @param style_id [String] Style id (w:styleId)
|
|
457
|
+
# @return [Boolean] true when the style was removed
|
|
458
|
+
def remove_style(style_id)
|
|
459
|
+
StyleCleanup.new(self).remove?(style_id)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Rename a style's display name (w:name) — Word's style rename.
|
|
463
|
+
# References (pStyle/rStyle/tblStyle) target the styleId, so they
|
|
464
|
+
# keep working unchanged.
|
|
465
|
+
#
|
|
466
|
+
# @param identifier [String] Style id (w:styleId) or current
|
|
467
|
+
# display name (w:name)
|
|
468
|
+
# @param new_name [String] New display name
|
|
469
|
+
# @return [Style, nil] The renamed style, or nil when not found
|
|
470
|
+
def rename_style(identifier, new_name)
|
|
471
|
+
style = styles_configuration.style(identifier)
|
|
472
|
+
return unless style
|
|
473
|
+
|
|
474
|
+
style.name = StyleName.new(val: new_name)
|
|
475
|
+
style
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Remove every style that no content references — Word's
|
|
479
|
+
# Styles-pane decluttering as one call. Default styles are kept.
|
|
480
|
+
#
|
|
481
|
+
# @return [Array<String>] Ids of removed styles
|
|
482
|
+
def remove_unused_styles
|
|
483
|
+
StyleCleanup.new(self).remove_unused
|
|
484
|
+
end
|
|
485
|
+
|
|
317
486
|
# Auto-transition from MS theme to Uniword equivalent
|
|
318
487
|
#
|
|
319
488
|
# Detects the MS theme in the document's embedded theme and replaces
|
|
@@ -376,6 +545,27 @@ module Uniword
|
|
|
376
545
|
def to_html_document
|
|
377
546
|
Uniword::Transformation::OoxmlToHtmlConverter.document_to_html(self)
|
|
378
547
|
end
|
|
548
|
+
|
|
549
|
+
private
|
|
550
|
+
|
|
551
|
+
# Ensure the document carries a theme part, creating it from a
|
|
552
|
+
# fresh parse of the bundled Office theme when absent (every
|
|
553
|
+
# document gets its own copy — no shared state).
|
|
554
|
+
#
|
|
555
|
+
# @param transformation [Themes::ThemeTransformation] Converter
|
|
556
|
+
# @return [void]
|
|
557
|
+
def ensure_theme!(transformation)
|
|
558
|
+
return if theme&.theme_elements
|
|
559
|
+
|
|
560
|
+
self.theme = transformation.default_office_theme
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Run model-level validation rules against this document.
|
|
564
|
+
#
|
|
565
|
+
# @return [Array<Validation::Report::ValidationIssue>] issues found
|
|
566
|
+
def structural_issues
|
|
567
|
+
Validation::Engine.run(Validation::Rules::ModelContext.new(self))
|
|
568
|
+
end
|
|
379
569
|
end
|
|
380
570
|
end
|
|
381
571
|
end
|
|
@@ -32,6 +32,10 @@ module Uniword
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
missing = endnote_entries.size - counts["endnote"]
|
|
35
|
+
return unless missing.positive?
|
|
36
|
+
|
|
37
|
+
# element_order from the parser may be frozen; never mutate it.
|
|
38
|
+
self.element_order = element_order.dup
|
|
35
39
|
missing.times do
|
|
36
40
|
element_order << Lutaml::Xml::Element.new("Element", "endnote")
|
|
37
41
|
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Wordprocessingml
|
|
5
|
+
# Replaces one font family with another across a document —
|
|
6
|
+
# Word's Home → Replace → Replace Fonts as an API.
|
|
7
|
+
#
|
|
8
|
+
# Covers style definitions and defaults, body content (paragraphs,
|
|
9
|
+
# tables, hyperlinks), headers and footers, footnotes, endnotes,
|
|
10
|
+
# comments, and numbering levels. Theme font references
|
|
11
|
+
# (asciiTheme/hAnsiTheme/...) are NOT rewritten — those are
|
|
12
|
+
# controlled by the theme's font scheme
|
|
13
|
+
# (see DocumentRoot#apply_font_scheme).
|
|
14
|
+
#
|
|
15
|
+
# @example Replace Calibri with Carlito throughout a document
|
|
16
|
+
# replacer = FontReplacer.new(from: "Calibri", to: "Carlito")
|
|
17
|
+
# replacer.replace(document)
|
|
18
|
+
# puts replacer.count # number of rFonts values rewritten
|
|
19
|
+
class FontReplacer
|
|
20
|
+
# RunFonts attributes rewritten by font replacement.
|
|
21
|
+
FONT_ATTRIBUTES = %i[ascii h_ansi east_asia cs].freeze
|
|
22
|
+
|
|
23
|
+
# Create a replacer.
|
|
24
|
+
#
|
|
25
|
+
# @param from [String] Font family to replace (exact match,
|
|
26
|
+
# Word semantics)
|
|
27
|
+
# @param to [String] Replacement font family
|
|
28
|
+
def initialize(from:, to:)
|
|
29
|
+
@from = from.to_s
|
|
30
|
+
@to = to.to_s
|
|
31
|
+
@count = 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Number of rFonts attribute values replaced so far.
|
|
35
|
+
#
|
|
36
|
+
# @return [Integer] Replacement count
|
|
37
|
+
attr_reader :count
|
|
38
|
+
|
|
39
|
+
# Replace @from with @to throughout the document (mutated).
|
|
40
|
+
#
|
|
41
|
+
# @param document [DocumentRoot] Document to rewrite
|
|
42
|
+
# @return [Integer] Replacement count
|
|
43
|
+
def replace(document)
|
|
44
|
+
replace_in_styles(document.styles_configuration)
|
|
45
|
+
replace_in_numbering(document.numbering_configuration)
|
|
46
|
+
replace_in_container(document.body)
|
|
47
|
+
part_containers(document).each do |container|
|
|
48
|
+
replace_in_paragraphs(container)
|
|
49
|
+
end
|
|
50
|
+
count
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Styles: docDefaults and every style definition.
|
|
56
|
+
def replace_in_styles(styles)
|
|
57
|
+
return unless styles
|
|
58
|
+
|
|
59
|
+
replace_in_run_properties(styles.doc_defaults&.rPrDefault&.rPr)
|
|
60
|
+
(styles.styles || []).each do |style|
|
|
61
|
+
replace_in_run_properties(style.rPr)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Numbering: every level of every abstract definition.
|
|
66
|
+
def replace_in_numbering(numbering)
|
|
67
|
+
return unless numbering
|
|
68
|
+
|
|
69
|
+
(numbering.definitions || []).each do |definition|
|
|
70
|
+
(definition.levels || []).each do |level|
|
|
71
|
+
replace_in_run_properties(level.rPr)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Containers outside the body: header/footer parts, note
|
|
77
|
+
# entries, and comments — model objects holding paragraphs.
|
|
78
|
+
def part_containers(document)
|
|
79
|
+
header_footer_containers(document) +
|
|
80
|
+
note_containers(document) + comment_containers(document)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def header_footer_containers(document)
|
|
84
|
+
(document.header_footer_parts || []).filter_map do |part|
|
|
85
|
+
part.content if part.content.is_a?(Lutaml::Model::Serializable)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def note_containers(document)
|
|
90
|
+
(document.footnotes&.footnote_entries || []) +
|
|
91
|
+
(document.endnotes&.endnote_entries || [])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def comment_containers(document)
|
|
95
|
+
document.comments&.comments || []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Walk a container's paragraphs, recursing into (nested) tables.
|
|
99
|
+
def replace_in_container(container)
|
|
100
|
+
return unless container
|
|
101
|
+
|
|
102
|
+
replace_in_paragraphs(container)
|
|
103
|
+
(container.tables || []).each { |table| replace_in_table(table) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def replace_in_table(table)
|
|
107
|
+
(table.rows || []).each do |row|
|
|
108
|
+
(row.cells || []).each { |cell| replace_in_container(cell) }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Walk only paragraphs — for note/comment/header/footer parts,
|
|
113
|
+
# whose models do not expose nested tables.
|
|
114
|
+
def replace_in_paragraphs(container)
|
|
115
|
+
(container.paragraphs || []).each do |paragraph|
|
|
116
|
+
replace_in_paragraph(paragraph)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def replace_in_paragraph(paragraph)
|
|
121
|
+
replace_in_runs(paragraph.runs)
|
|
122
|
+
(paragraph.hyperlinks || []).each do |hyperlink|
|
|
123
|
+
replace_in_runs(hyperlink.runs)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def replace_in_runs(runs)
|
|
128
|
+
(runs || []).each do |run|
|
|
129
|
+
replace_in_run_properties(run.properties)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Rewrite matching RunFonts attribute values in place.
|
|
134
|
+
def replace_in_run_properties(run_properties)
|
|
135
|
+
fonts = run_properties&.fonts
|
|
136
|
+
return unless fonts
|
|
137
|
+
|
|
138
|
+
FONT_ATTRIBUTES.each do |attr|
|
|
139
|
+
next unless fonts.method(attr).call == @from
|
|
140
|
+
|
|
141
|
+
fonts.method(:"#{attr}=").call(@to)
|
|
142
|
+
@count += 1
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -32,6 +32,10 @@ module Uniword
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
missing = footnote_entries.size - counts["footnote"]
|
|
35
|
+
return unless missing.positive?
|
|
36
|
+
|
|
37
|
+
# element_order from the parser may be frozen; never mutate it.
|
|
38
|
+
self.element_order = element_order.dup
|
|
35
39
|
missing.times do
|
|
36
40
|
element_order << Lutaml::Xml::Element.new("Element", "footnote")
|
|
37
41
|
end
|
|
@@ -17,20 +17,6 @@ module Uniword
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# Tabs container for numbering level
|
|
21
|
-
#
|
|
22
|
-
# Element: <w:tabs>
|
|
23
|
-
class Tabs < Lutaml::Model::Serializable
|
|
24
|
-
attribute :tab, Tab, collection: true
|
|
25
|
-
|
|
26
|
-
xml do
|
|
27
|
-
element "tabs"
|
|
28
|
-
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
29
|
-
mixed_content
|
|
30
|
-
map_element "tab", to: :tab, render_nil: false
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
20
|
# Numbering level suffix type
|
|
35
21
|
#
|
|
36
22
|
# Element: <w:suff>
|
|
@@ -63,6 +49,10 @@ module Uniword
|
|
|
63
49
|
#
|
|
64
50
|
# Generated from OOXML schema: wordprocessingml.yml
|
|
65
51
|
# Element: <w:lvl>
|
|
52
|
+
#
|
|
53
|
+
# Per ECMA-376 CT_Lvl, `w:ind` and `w:tabs` are NOT direct children of
|
|
54
|
+
# `w:lvl` — they belong to the level's `w:pPr`. They are therefore not
|
|
55
|
+
# mapped here; use `pPr.indentation` / `pPr.tabs`.
|
|
66
56
|
class Level < Lutaml::Model::Serializable
|
|
67
57
|
attribute :ilvl, :integer
|
|
68
58
|
attribute :tentative, :string
|
|
@@ -75,8 +65,6 @@ module Uniword
|
|
|
75
65
|
attribute :lvlText, LvlText
|
|
76
66
|
attribute :lvlJc, LvlJc
|
|
77
67
|
attribute :pPr, ParagraphProperties
|
|
78
|
-
attribute :tabs, Tabs
|
|
79
|
-
attribute :ind, Ind
|
|
80
68
|
attribute :rPr, RunProperties
|
|
81
69
|
|
|
82
70
|
xml do
|
|
@@ -95,8 +83,6 @@ module Uniword
|
|
|
95
83
|
map_element "lvlText", to: :lvlText, render_nil: false
|
|
96
84
|
map_element "lvlJc", to: :lvlJc, render_nil: false
|
|
97
85
|
map_element "pPr", to: :pPr, render_nil: false
|
|
98
|
-
map_element "tabs", to: :tabs, render_nil: false
|
|
99
|
-
map_element "ind", to: :ind, render_nil: false
|
|
100
86
|
map_element "rPr", to: :rPr, render_nil: false
|
|
101
87
|
end
|
|
102
88
|
|
|
@@ -122,12 +108,12 @@ module Uniword
|
|
|
122
108
|
|
|
123
109
|
# Get left indent (convenience method - returns integer)
|
|
124
110
|
def left_indent_value
|
|
125
|
-
|
|
111
|
+
pPr&.indentation&.left.to_i
|
|
126
112
|
end
|
|
127
113
|
|
|
128
114
|
# Get hanging indent (convenience method - returns integer)
|
|
129
115
|
def hanging_indent_value
|
|
130
|
-
|
|
116
|
+
pPr&.indentation&.hanging.to_i
|
|
131
117
|
end
|
|
132
118
|
|
|
133
119
|
# Get level text value (convenience method - returns string)
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
-
require_relative "math_font"
|
|
5
|
-
require_relative "brk_bin"
|
|
6
|
-
require_relative "brk_bin_sub"
|
|
7
|
-
require_relative "small_frac"
|
|
8
|
-
require_relative "disp_def"
|
|
9
|
-
require_relative "l_margin"
|
|
10
|
-
require_relative "r_margin"
|
|
11
|
-
require_relative "def_jc"
|
|
12
|
-
require_relative "wrap_indent"
|
|
13
|
-
require_relative "int_lim"
|
|
14
|
-
require_relative "nary_lim"
|
|
15
4
|
|
|
16
5
|
module Uniword
|
|
17
6
|
module Wordprocessingml
|
|
@@ -80,8 +80,10 @@ module Uniword
|
|
|
80
80
|
start: Uniword::Wordprocessingml::Start.new(val: 1),
|
|
81
81
|
numFmt: Uniword::Wordprocessingml::NumFmt.new(val: "decimal"),
|
|
82
82
|
lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
|
|
84
|
+
indentation: Uniword::Properties::Indentation.new(
|
|
85
|
+
left: 720 * (i + 1), hanging: 360,
|
|
86
|
+
),
|
|
85
87
|
),
|
|
86
88
|
)
|
|
87
89
|
end
|
|
@@ -104,8 +106,10 @@ module Uniword
|
|
|
104
106
|
numFmt: Uniword::Wordprocessingml::NumFmt.new(val: "bullet"),
|
|
105
107
|
lvlText: Uniword::Wordprocessingml::LvlText.new(val: level_char),
|
|
106
108
|
lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
|
|
110
|
+
indentation: Uniword::Properties::Indentation.new(
|
|
111
|
+
left: 720 * (i + 1), hanging: 360,
|
|
112
|
+
),
|
|
109
113
|
),
|
|
110
114
|
rPr: Uniword::Wordprocessingml::RunProperties.new(
|
|
111
115
|
fonts: Properties::RunFonts.new(ascii: "Symbol",
|
|
@@ -126,8 +130,10 @@ module Uniword
|
|
|
126
130
|
start: Uniword::Wordprocessingml::Start.new(val: 1),
|
|
127
131
|
numFmt: Uniword::Wordprocessingml::NumFmt.new(val: format),
|
|
128
132
|
lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
|
|
134
|
+
indentation: Uniword::Properties::Indentation.new(
|
|
135
|
+
left: 720 * (i + 1), hanging: 360,
|
|
136
|
+
),
|
|
131
137
|
),
|
|
132
138
|
)
|
|
133
139
|
end
|
|
@@ -144,8 +150,10 @@ module Uniword
|
|
|
144
150
|
start: Uniword::Wordprocessingml::Start.new(val: 1),
|
|
145
151
|
numFmt: Uniword::Wordprocessingml::NumFmt.new(val: format),
|
|
146
152
|
lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
|
|
154
|
+
indentation: Uniword::Properties::Indentation.new(
|
|
155
|
+
left: 720 * (i + 1), hanging: 360,
|
|
156
|
+
),
|
|
149
157
|
),
|
|
150
158
|
)
|
|
151
159
|
end
|