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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/lib/uniword/builder/chart_builder.rb +3 -5
  4. data/lib/uniword/builder/image_builder.rb +5 -5
  5. data/lib/uniword/document_factory.rb +10 -24
  6. data/lib/uniword/docx/custom_xml_item.rb +8 -0
  7. data/lib/uniword/docx/image_part.rb +83 -0
  8. data/lib/uniword/docx/package.rb +22 -402
  9. data/lib/uniword/docx/package_defaults.rb +36 -32
  10. data/lib/uniword/docx/package_serialization.rb +46 -7
  11. data/lib/uniword/docx/part.rb +19 -0
  12. data/lib/uniword/docx/part_collection.rb +15 -7
  13. data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
  14. data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
  15. data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
  16. data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
  17. data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
  18. data/lib/uniword/docx/part_loader/load_context.rb +85 -0
  19. data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
  20. data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
  21. data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
  22. data/lib/uniword/docx/part_loader.rb +92 -0
  23. data/lib/uniword/docx/raw_part.rb +75 -0
  24. data/lib/uniword/docx/reconciler/helpers.rb +9 -30
  25. data/lib/uniword/docx/reconciler/package_structure.rb +4 -5
  26. data/lib/uniword/docx/reconciler/referential_integrity.rb +4 -79
  27. data/lib/uniword/docx.rb +3 -0
  28. data/lib/uniword/images/image_manager.rb +13 -13
  29. data/lib/uniword/ooxml/element_order.rb +94 -0
  30. data/lib/uniword/ooxml/part_definition.rb +164 -6
  31. data/lib/uniword/ooxml/part_registry.rb +238 -45
  32. data/lib/uniword/ooxml/relationships/image_relationship.rb +5 -2
  33. data/lib/uniword/ooxml.rb +1 -0
  34. data/lib/uniword/toc/toc_generator.rb +6 -7
  35. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
  36. data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
  37. data/lib/uniword/validation/opc_validator.rb +16 -1
  38. data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -0
  39. data/lib/uniword/version.rb +1 -1
  40. data/lib/uniword/wordprocessingml/document_root.rb +29 -220
  41. data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
  42. data/lib/uniword/wordprocessingml/endnotes.rb +2 -3
  43. data/lib/uniword/wordprocessingml/footnotes.rb +2 -3
  44. data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
  45. data/lib/uniword/wordprocessingml.rb +1 -2
  46. metadata +16 -2
@@ -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
@@ -34,10 +34,9 @@ module Uniword
34
34
  missing = endnote_entries.size - counts["endnote"]
35
35
  return unless missing.positive?
36
36
 
37
- # element_order from the parser may be frozen; never mutate it.
38
- self.element_order = element_order.dup
37
+ order = Ooxml::ElementOrder.mutable_order(self)
39
38
  missing.times do
40
- element_order << Lutaml::Xml::Element.new("Element", "endnote")
39
+ order << Lutaml::Xml::Element.new("Element", "endnote")
41
40
  end
42
41
  end
43
42
 
@@ -34,10 +34,9 @@ module Uniword
34
34
  missing = footnote_entries.size - counts["footnote"]
35
35
  return unless missing.positive?
36
36
 
37
- # element_order from the parser may be frozen; never mutate it.
38
- self.element_order = element_order.dup
37
+ order = Ooxml::ElementOrder.mutable_order(self)
39
38
  missing.times do
40
- element_order << Lutaml::Xml::Element.new("Element", "footnote")
39
+ order << Lutaml::Xml::Element.new("Element", "footnote")
41
40
  end
42
41
  end
43
42
 
@@ -30,21 +30,6 @@ module Uniword
30
30
  end
31
31
  end
32
32
 
33
- # Indentation for numbering level
34
- #
35
- # Element: <w:ind>
36
- class Ind < Lutaml::Model::Serializable
37
- attribute :left, :string
38
- attribute :hanging, :string
39
-
40
- xml do
41
- element "ind"
42
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
43
- map_attribute "left", to: :left
44
- map_attribute "hanging", to: :hanging
45
- end
46
- end
47
-
48
33
  # Run fonts for numbering
49
34
  #
50
35
  # Element: <w:rFonts>
@@ -4,6 +4,7 @@ module Uniword
4
4
  module Wordprocessingml
5
5
  # Core document structure
6
6
  autoload :DocumentRoot, "uniword/wordprocessingml/document_root"
7
+ autoload :DocumentStyling, "uniword/wordprocessingml/document_styling"
7
8
  autoload :Body, "uniword/wordprocessingml/body"
8
9
  autoload :Paragraph, "uniword/wordprocessingml/paragraph"
9
10
  autoload :Run, "uniword/wordprocessingml/run"
@@ -165,9 +166,7 @@ module Uniword
165
166
  autoload :NumFmt, "uniword/wordprocessingml/num_fmt"
166
167
  autoload :LvlText, "uniword/wordprocessingml/lvl_text"
167
168
  autoload :LvlJc, "uniword/wordprocessingml/lvl_jc"
168
- autoload :Ind, "uniword/wordprocessingml/numbering_elements"
169
169
  autoload :RFonts, "uniword/wordprocessingml/numbering_elements"
170
- autoload :Tabs, "uniword/wordprocessingml/level"
171
170
  autoload :PStyle, "uniword/wordprocessingml/level"
172
171
  autoload :NumberingConfiguration,
173
172
  "uniword/wordprocessingml/numbering_configuration"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uniword
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-19 00:00:00.000000000 Z
11
+ date: 2026-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger
@@ -822,13 +822,25 @@ files:
822
822
  - lib/uniword/docx/header_footer_part_collection.rb
823
823
  - lib/uniword/docx/header_footer_view.rb
824
824
  - lib/uniword/docx/id_allocator.rb
825
+ - lib/uniword/docx/image_part.rb
825
826
  - lib/uniword/docx/package.rb
826
827
  - lib/uniword/docx/package_defaults.rb
827
828
  - lib/uniword/docx/package_integrity_checker.rb
828
829
  - lib/uniword/docx/package_serialization.rb
829
830
  - lib/uniword/docx/part.rb
830
831
  - lib/uniword/docx/part_collection.rb
832
+ - lib/uniword/docx/part_loader.rb
833
+ - lib/uniword/docx/part_loader/chart_loader.rb
834
+ - lib/uniword/docx/part_loader/custom_xml_loader.rb
835
+ - lib/uniword/docx/part_loader/embedding_loader.rb
836
+ - lib/uniword/docx/part_loader/header_footer_loader.rb
837
+ - lib/uniword/docx/part_loader/image_loader.rb
838
+ - lib/uniword/docx/part_loader/load_context.rb
839
+ - lib/uniword/docx/part_loader/raw_part_loader.rb
840
+ - lib/uniword/docx/part_loader/theme_media_loader.rb
841
+ - lib/uniword/docx/part_loader/xml_model_loader.rb
831
842
  - lib/uniword/docx/profile.rb
843
+ - lib/uniword/docx/raw_part.rb
832
844
  - lib/uniword/docx/reconciler.rb
833
845
  - lib/uniword/docx/reconciler/body.rb
834
846
  - lib/uniword/docx/reconciler/fix.rb
@@ -1109,6 +1121,7 @@ files:
1109
1121
  - lib/uniword/ooxml/core_properties.rb
1110
1122
  - lib/uniword/ooxml/custom_properties.rb
1111
1123
  - lib/uniword/ooxml/dotx_package.rb
1124
+ - lib/uniword/ooxml/element_order.rb
1112
1125
  - lib/uniword/ooxml/namespaces.rb
1113
1126
  - lib/uniword/ooxml/package_file.rb
1114
1127
  - lib/uniword/ooxml/part_definition.rb
@@ -1669,6 +1682,7 @@ files:
1669
1682
  - lib/uniword/wordprocessingml/doc_vars.rb
1670
1683
  - lib/uniword/wordprocessingml/document_root.rb
1671
1684
  - lib/uniword/wordprocessingml/document_root/feature_facade.rb
1685
+ - lib/uniword/wordprocessingml/document_styling.rb
1672
1686
  - lib/uniword/wordprocessingml/drawing.rb
1673
1687
  - lib/uniword/wordprocessingml/emboss.rb
1674
1688
  - lib/uniword/wordprocessingml/endnote.rb