uniword 1.3.0 → 1.4.1
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 +90 -0
- data/lib/uniword/builder/chart_builder.rb +3 -5
- data/lib/uniword/builder/image_builder.rb +5 -5
- data/lib/uniword/document_factory.rb +10 -24
- data/lib/uniword/docx/custom_xml_item.rb +8 -0
- data/lib/uniword/docx/image_part.rb +83 -0
- data/lib/uniword/docx/package.rb +22 -402
- data/lib/uniword/docx/package_defaults.rb +36 -32
- data/lib/uniword/docx/package_serialization.rb +46 -7
- data/lib/uniword/docx/part.rb +19 -0
- data/lib/uniword/docx/part_collection.rb +15 -7
- 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/helpers.rb +9 -30
- data/lib/uniword/docx/reconciler/package_structure.rb +4 -5
- data/lib/uniword/docx/reconciler/referential_integrity.rb +4 -79
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/images/image_manager.rb +13 -13
- data/lib/uniword/ooxml/element_order.rb +94 -0
- data/lib/uniword/ooxml/part_definition.rb +164 -6
- data/lib/uniword/ooxml/part_registry.rb +238 -45
- data/lib/uniword/ooxml/relationships/image_relationship.rb +5 -2
- data/lib/uniword/ooxml.rb +1 -0
- data/lib/uniword/toc/toc_generator.rb +6 -7
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
- data/lib/uniword/validation/opc_validator.rb +16 -1
- data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -0
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +29 -220
- data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
- data/lib/uniword/wordprocessingml/endnotes.rb +2 -3
- data/lib/uniword/wordprocessingml/footnotes.rb +2 -3
- data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
- data/lib/uniword/wordprocessingml.rb +1 -2
- metadata +16 -2
|
@@ -91,14 +91,13 @@ module Uniword
|
|
|
91
91
|
# Add the SDT to element_order so it serializes correctly
|
|
92
92
|
return unless body.element_order
|
|
93
93
|
|
|
94
|
-
# element_order from the parser may be frozen; never mutate it.
|
|
95
|
-
body.element_order = body.element_order.dup
|
|
96
94
|
insert_element = Lutaml::Xml::Element.new("Element", "sdt")
|
|
97
|
-
if position.positive? && body.element_order.size >= position
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
index = if position.positive? && body.element_order.size >= position
|
|
96
|
+
position
|
|
97
|
+
else
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
Ooxml::ElementOrder.insert_at(body, index, insert_element)
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
# Update an existing TOC in the document.
|
|
@@ -219,11 +219,7 @@ module Uniword
|
|
|
219
219
|
def resolve_image_target(embed_id)
|
|
220
220
|
return nil unless @image_parts
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
return nil unless entry
|
|
224
|
-
|
|
225
|
-
image_data = entry[1]
|
|
226
|
-
image_data[:target]
|
|
222
|
+
@image_parts[embed_id]&.target
|
|
227
223
|
end
|
|
228
224
|
|
|
229
225
|
# Apply run formatting (text must already be HTML-escaped)
|
|
@@ -63,8 +63,8 @@ module Uniword
|
|
|
63
63
|
# Build filelist.xml part
|
|
64
64
|
def build_filelist_part
|
|
65
65
|
image_entries = if @document.image_parts && !@document.image_parts.empty?
|
|
66
|
-
@document.image_parts.map do |_r_id,
|
|
67
|
-
%(<o:File HRef="#{
|
|
66
|
+
@document.image_parts.map do |_r_id, part|
|
|
67
|
+
%(<o:File HRef="#{part.target}"/>)
|
|
68
68
|
end.join("\n ")
|
|
69
69
|
else
|
|
70
70
|
""
|
|
@@ -91,12 +91,12 @@ module Uniword
|
|
|
91
91
|
def build_image_parts
|
|
92
92
|
return [] unless @document.image_parts && !@document.image_parts.empty?
|
|
93
93
|
|
|
94
|
-
@document.image_parts.map do |_r_id,
|
|
94
|
+
@document.image_parts.map do |_r_id, image_part|
|
|
95
95
|
part = Uniword::Mhtml::ImagePart.new
|
|
96
|
-
part.content_type =
|
|
96
|
+
part.content_type = image_part.content_type || "image/png"
|
|
97
97
|
part.content_transfer_encoding = "base64"
|
|
98
|
-
part.raw_content =
|
|
99
|
-
part.content_location = "file:///C:/D057922B/#{document_name}.fld/#{
|
|
98
|
+
part.raw_content = image_part.data
|
|
99
|
+
part.content_location = "file:///C:/D057922B/#{document_name}.fld/#{image_part.target}"
|
|
100
100
|
part
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -221,7 +221,22 @@ module Uniword
|
|
|
221
221
|
def resolve_target(base_dir, target)
|
|
222
222
|
return target[1..] if target.start_with?("/")
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
normalize_package_path(
|
|
225
|
+
base_dir.empty? ? target : File.join(base_dir, target),
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Lexically normalize a package-relative path (resolve "." and
|
|
230
|
+
# ".." segments). Deliberately avoids File.expand_path, which
|
|
231
|
+
# prepends a drive letter on Windows.
|
|
232
|
+
def normalize_package_path(path)
|
|
233
|
+
segments = []
|
|
234
|
+
path.split("/").each do |segment|
|
|
235
|
+
next if segment.empty? || segment == "."
|
|
236
|
+
|
|
237
|
+
segment == ".." ? segments.pop : segments << segment
|
|
238
|
+
end
|
|
239
|
+
segments.join("/")
|
|
225
240
|
end
|
|
226
241
|
end
|
|
227
242
|
end
|
|
@@ -20,6 +20,11 @@ module Uniword
|
|
|
20
20
|
# validator = XmlSchemaValidator.new("xml_schema" => { "xsd_validation" => true })
|
|
21
21
|
# result = validator.validate('/path/to/document.docx')
|
|
22
22
|
class XmlSchemaValidator < LayerValidator
|
|
23
|
+
# Markup Compatibility namespace — the mc:Ignorable attribute
|
|
24
|
+
# itself lives here and is stripped during MCE preprocessing.
|
|
25
|
+
MCE_NAMESPACE_URI =
|
|
26
|
+
"http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
27
|
+
|
|
23
28
|
def layer_name
|
|
24
29
|
"XSD Schema"
|
|
25
30
|
end
|
|
@@ -83,6 +88,7 @@ module Uniword
|
|
|
83
88
|
content = entry.get_input_stream.read
|
|
84
89
|
schema = registry.load_schema(schema_path)
|
|
85
90
|
doc = Nokogiri::XML(content)
|
|
91
|
+
preprocess_mce(doc)
|
|
86
92
|
|
|
87
93
|
schema.validate(doc).each do |error|
|
|
88
94
|
issues << Report::ValidationIssue.new(
|
|
@@ -114,6 +120,46 @@ module Uniword
|
|
|
114
120
|
end
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
# Strip MCE-ignorable ATTRIBUTES per the part's own mc:Ignorable
|
|
124
|
+
# declaration (w14:paraId/textId and the mc:Ignorable attribute
|
|
125
|
+
# itself), so XSD validation is not drowned in false errors for
|
|
126
|
+
# content every real document carries.
|
|
127
|
+
#
|
|
128
|
+
# Extension ELEMENTS are deliberately kept: removing them changes
|
|
129
|
+
# libxml2's content-model evaluation and can expose pre-existing
|
|
130
|
+
# Word-vs-schema quirks (e.g. rsids placed late, tolerated via the
|
|
131
|
+
# sequence's trailing xsd:any) as false baseline errors.
|
|
132
|
+
# Operates in place on the parsed document; never touches the
|
|
133
|
+
# packaged bytes.
|
|
134
|
+
#
|
|
135
|
+
# @param doc [Nokogiri::XML::Document] parsed part to clean
|
|
136
|
+
# @return [void]
|
|
137
|
+
def preprocess_mce(doc)
|
|
138
|
+
root = doc.root
|
|
139
|
+
return unless root
|
|
140
|
+
|
|
141
|
+
ignorable = root["Ignorable"] || root["mc:Ignorable"]
|
|
142
|
+
return if ignorable.nil? || ignorable.empty?
|
|
143
|
+
|
|
144
|
+
uris = ignorable.split(/\s+/).filter_map do |prefix|
|
|
145
|
+
root.namespaces["xmlns:#{prefix}"]
|
|
146
|
+
end
|
|
147
|
+
# The mc:Ignorable attribute itself is MCE machinery and is
|
|
148
|
+
# undeclared on most root elements in the transitional XSDs.
|
|
149
|
+
strip_mce_attributes(root, uris + [MCE_NAMESPACE_URI])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Remove attributes in ignorable namespaces from every element.
|
|
153
|
+
def strip_mce_attributes(doc, uris)
|
|
154
|
+
doc.traverse do |node|
|
|
155
|
+
next unless node.element?
|
|
156
|
+
|
|
157
|
+
node.attribute_nodes.each do |attr|
|
|
158
|
+
attr.remove if uris.include?(attr.namespace&.href)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
117
163
|
def check_unknown_namespaces(registry, content, part_name, issues)
|
|
118
164
|
ns_uris = registry.detect_namespaces(content)
|
|
119
165
|
unknown = registry.unknown_namespaces(ns_uris)
|
data/lib/uniword/version.rb
CHANGED
|
@@ -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
|
|
@@ -96,9 +97,6 @@ module Uniword
|
|
|
96
97
|
attr_accessor :theme, :raw_html, :revisions, :comments
|
|
97
98
|
# Footnotes and endnotes (separate XML parts in DOCX package)
|
|
98
99
|
attr_accessor :footnotes, :endnotes
|
|
99
|
-
# Image parts to embed in the DOCX package
|
|
100
|
-
# Hash: r_id => { path: String, data: String, content_type: String, target: String }
|
|
101
|
-
attr_accessor :image_parts
|
|
102
100
|
# Bibliography sources for sources.xml
|
|
103
101
|
attr_accessor :bibliography_sources
|
|
104
102
|
# Round-trip parts (copied from DocxPackage during load)
|
|
@@ -154,6 +152,19 @@ module Uniword
|
|
|
154
152
|
footers.replace(value)
|
|
155
153
|
end
|
|
156
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
|
+
|
|
157
168
|
# Chart parts to embed in the DOCX package, keyed by rId.
|
|
158
169
|
#
|
|
159
170
|
# @return [Docx::PartCollection] rId => ChartPart
|
|
@@ -178,6 +189,21 @@ module Uniword
|
|
|
178
189
|
embeddings.replace_all(value)
|
|
179
190
|
end
|
|
180
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
|
+
|
|
181
207
|
# Get app_properties (lazy initialization)
|
|
182
208
|
#
|
|
183
209
|
# @return [Uniword::Ooxml::AppProperties] The app properties object
|
|
@@ -327,211 +353,6 @@ module Uniword
|
|
|
327
353
|
|
|
328
354
|
# Apply theme to document
|
|
329
355
|
#
|
|
330
|
-
# Applies a Uniword theme by name, updating doc defaults and
|
|
331
|
-
# built-in heading/hyperlink styles to reference the theme.
|
|
332
|
-
#
|
|
333
|
-
# @param name [String, Symbol] Theme slug (e.g., 'meridian', 'corporate')
|
|
334
|
-
# @param options [Hash] optional overrides
|
|
335
|
-
# @option options [Hash] :colors override specific color keys
|
|
336
|
-
# @option options [String] :major_font override major font
|
|
337
|
-
# @option options [String] :minor_font override minor font
|
|
338
|
-
# @return [self] For method chaining
|
|
339
|
-
def apply_theme(name, **options)
|
|
340
|
-
friendly = Themes::Theme.load(name.to_s)
|
|
341
|
-
|
|
342
|
-
options[:colors]&.each do |key, value|
|
|
343
|
-
friendly.color_scheme[key.to_s] = value
|
|
344
|
-
end
|
|
345
|
-
friendly.font_scheme.major_font = options[:major_font] if options[:major_font]
|
|
346
|
-
friendly.font_scheme.minor_font = options[:minor_font] if options[:minor_font]
|
|
347
|
-
|
|
348
|
-
word_theme = Themes::ThemeTransformation.new.to_word(friendly)
|
|
349
|
-
Themes::ThemeApplicator.new.apply(word_theme, self)
|
|
350
|
-
self
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
# Apply theme from .thmx file
|
|
354
|
-
#
|
|
355
|
-
# @param path [String] Path to .thmx file
|
|
356
|
-
# @param variant [String, Integer, nil] Optional variant
|
|
357
|
-
# @return [self] For method chaining
|
|
358
|
-
def apply_theme_file(path, variant: nil)
|
|
359
|
-
loader = Themes::ThemeLoader.new
|
|
360
|
-
self.theme = if variant
|
|
361
|
-
loader.load_with_variant(path, variant)
|
|
362
|
-
else
|
|
363
|
-
loader.load(path)
|
|
364
|
-
end
|
|
365
|
-
self
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# Apply StyleSet to document
|
|
369
|
-
#
|
|
370
|
-
# @param name [String, Symbol] StyleSet slug (e.g., 'signature', 'heritage')
|
|
371
|
-
# @param strategy [Symbol] Application strategy (:keep_existing, :replace, :rename)
|
|
372
|
-
# @return [self] For method chaining
|
|
373
|
-
def apply_styleset(name, strategy: :keep_existing)
|
|
374
|
-
styleset = Uniword::Stylesets::YamlStyleSetLoader.load_bundled(name.to_s)
|
|
375
|
-
styleset.apply_to(self, strategy: strategy)
|
|
376
|
-
self
|
|
377
|
-
end
|
|
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
|
-
|
|
486
|
-
# Auto-transition from MS theme to Uniword equivalent
|
|
487
|
-
#
|
|
488
|
-
# Detects the MS theme in the document's embedded theme and replaces
|
|
489
|
-
# it with the corresponding Uniword theme (font-substituted, renamed).
|
|
490
|
-
#
|
|
491
|
-
# @return [Hash, nil] { uniword_slug:, ms_name: } or nil if no match
|
|
492
|
-
#
|
|
493
|
-
# @example
|
|
494
|
-
# result = doc.auto_transition_theme
|
|
495
|
-
# puts "Transitioned from #{result[:ms_name]} to #{result[:uniword_slug]}"
|
|
496
|
-
def auto_transition_theme
|
|
497
|
-
Resource::ThemeTransition.auto_transition!(self)
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# Apply theme from another document
|
|
501
|
-
#
|
|
502
|
-
# @param source_path [String] Path to source .docx file
|
|
503
|
-
# @return [self] For method chaining
|
|
504
|
-
def apply_theme_from(source_path)
|
|
505
|
-
source_doc = Uniword.load(source_path)
|
|
506
|
-
self.theme = source_doc.theme.dup if source_doc.theme
|
|
507
|
-
self
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
# Apply styles from another document
|
|
511
|
-
#
|
|
512
|
-
# @param source_path [String] Path to source .docx file
|
|
513
|
-
# @param strategy [Symbol] Conflict resolution strategy
|
|
514
|
-
# @return [self] For method chaining
|
|
515
|
-
def apply_styles_from(source_path, strategy: :keep_existing)
|
|
516
|
-
source_doc = Uniword.load(source_path)
|
|
517
|
-
styles_configuration.merge(source_doc.styles_configuration,
|
|
518
|
-
conflict_resolution: strategy)
|
|
519
|
-
self
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
# Apply both theme and styles from a template document
|
|
523
|
-
#
|
|
524
|
-
# @param template_path [String] Path to template .docx file
|
|
525
|
-
# @param strategy [Symbol] Conflict resolution strategy for styles
|
|
526
|
-
# @return [self] For method chaining
|
|
527
|
-
def apply_template(template_path, strategy: :keep_existing)
|
|
528
|
-
template_doc = Uniword.load(template_path)
|
|
529
|
-
self.theme = template_doc.theme.dup if template_doc.theme
|
|
530
|
-
styles_configuration.merge(template_doc.styles_configuration,
|
|
531
|
-
conflict_resolution: strategy)
|
|
532
|
-
self
|
|
533
|
-
end
|
|
534
|
-
|
|
535
356
|
# Custom inspect for readable output
|
|
536
357
|
#
|
|
537
358
|
# @return [String] Human-readable representation
|
|
@@ -548,18 +369,6 @@ module Uniword
|
|
|
548
369
|
|
|
549
370
|
private
|
|
550
371
|
|
|
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
372
|
# Run model-level validation rules against this document.
|
|
564
373
|
#
|
|
565
374
|
# @return [Array<Validation::Report::ValidationIssue>] issues found
|