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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +223 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/config/ooxml/schemas/shared_types.yml +5 -4
  5. data/data/schemas/ecma/dc.xsd +118 -0
  6. data/data/schemas/ecma/dcmitype.xsd +50 -0
  7. data/data/schemas/ecma/dcterms.xsd +331 -0
  8. data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
  9. data/data/schemas/ecma/xml.xsd +117 -0
  10. data/lib/uniword/builder/bibliography_builder.rb +1 -1
  11. data/lib/uniword/builder/chart_builder.rb +19 -9
  12. data/lib/uniword/builder/comment_anchorer.rb +221 -0
  13. data/lib/uniword/builder/document_builder.rb +97 -6
  14. data/lib/uniword/builder/image_builder.rb +6 -5
  15. data/lib/uniword/builder/run_utils.rb +2 -0
  16. data/lib/uniword/builder.rb +1 -0
  17. data/lib/uniword/cli/fonts_cli.rb +52 -0
  18. data/lib/uniword/cli/main.rb +110 -11
  19. data/lib/uniword/cli/page_cli.rb +85 -0
  20. data/lib/uniword/cli/styles_cli.rb +204 -0
  21. data/lib/uniword/cli/theme_cli.rb +88 -0
  22. data/lib/uniword/cli/toc_cli.rb +22 -2
  23. data/lib/uniword/comment.rb +20 -15
  24. data/lib/uniword/comment_range.rb +3 -3
  25. data/lib/uniword/comments_part.rb +51 -19
  26. data/lib/uniword/configuration/configuration_loader.rb +5 -3
  27. data/lib/uniword/configuration.rb +93 -1
  28. data/lib/uniword/content_types.rb +19 -39
  29. data/lib/uniword/document_factory.rb +9 -1
  30. data/lib/uniword/document_writer.rb +16 -5
  31. data/lib/uniword/docx/chart_part.rb +34 -0
  32. data/lib/uniword/docx/custom_xml_item.rb +83 -0
  33. data/lib/uniword/docx/header_footer_part.rb +140 -0
  34. data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
  35. data/lib/uniword/docx/header_footer_view.rb +225 -0
  36. data/lib/uniword/docx/id_allocator.rb +130 -36
  37. data/lib/uniword/docx/package.rb +223 -35
  38. data/lib/uniword/docx/package_defaults.rb +58 -78
  39. data/lib/uniword/docx/package_integrity_checker.rb +312 -0
  40. data/lib/uniword/docx/package_serialization.rb +103 -231
  41. data/lib/uniword/docx/part.rb +106 -0
  42. data/lib/uniword/docx/part_collection.rb +123 -0
  43. data/lib/uniword/docx/reconciler/body.rb +59 -68
  44. data/lib/uniword/docx/reconciler/fix.rb +46 -0
  45. data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
  46. data/lib/uniword/docx/reconciler/helpers.rb +9 -1
  47. data/lib/uniword/docx/reconciler/notes.rb +17 -8
  48. data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
  49. data/lib/uniword/docx/reconciler/parts.rb +36 -13
  50. data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
  51. data/lib/uniword/docx/reconciler/tables.rb +78 -16
  52. data/lib/uniword/docx/reconciler/theme.rb +7 -4
  53. data/lib/uniword/docx/reconciler.rb +36 -14
  54. data/lib/uniword/docx.rb +10 -0
  55. data/lib/uniword/drawingml/blip.rb +0 -1
  56. data/lib/uniword/drawingml/color_scheme.rb +5 -6
  57. data/lib/uniword/drawingml/font_scheme.rb +4 -8
  58. data/lib/uniword/drawingml/theme.rb +12 -6
  59. data/lib/uniword/errors.rb +8 -1
  60. data/lib/uniword/footer.rb +4 -0
  61. data/lib/uniword/header.rb +4 -0
  62. data/lib/uniword/hyperlink.rb +1 -1
  63. data/lib/uniword/image.rb +6 -6
  64. data/lib/uniword/lazy_loader.rb +1 -2
  65. data/lib/uniword/model_attribute_access.rb +20 -4
  66. data/lib/uniword/ooxml/dotx_package.rb +42 -2
  67. data/lib/uniword/ooxml/part_definition.rb +171 -0
  68. data/lib/uniword/ooxml/part_registry.rb +311 -0
  69. data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
  70. data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
  71. data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
  72. data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
  73. data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
  74. data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
  75. data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
  76. data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
  77. data/lib/uniword/ooxml/types.rb +10 -0
  78. data/lib/uniword/ooxml.rb +2 -0
  79. data/lib/uniword/properties/alignment.rb +8 -3
  80. data/lib/uniword/properties/border.rb +52 -6
  81. data/lib/uniword/properties/cell_vertical_align.rb +4 -1
  82. data/lib/uniword/properties/color_value.rb +8 -3
  83. data/lib/uniword/properties/highlight.rb +9 -5
  84. data/lib/uniword/properties/shading.rb +4 -3
  85. data/lib/uniword/properties/tab_stop.rb +6 -1
  86. data/lib/uniword/properties/table_justification.rb +4 -1
  87. data/lib/uniword/properties/underline.rb +4 -2
  88. data/lib/uniword/properties/vertical_align.rb +6 -3
  89. data/lib/uniword/review/review_manager.rb +23 -9
  90. data/lib/uniword/revision.rb +9 -9
  91. data/lib/uniword/schema/model_generator.rb +31 -15
  92. data/lib/uniword/shared_types/hex_color.rb +4 -1
  93. data/lib/uniword/shared_types/pixel_measure.rb +4 -1
  94. data/lib/uniword/shared_types/point_measure.rb +4 -1
  95. data/lib/uniword/shared_types/text_alignment.rb +6 -1
  96. data/lib/uniword/shared_types/twips_measure.rb +4 -1
  97. data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
  98. data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
  99. data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
  100. data/lib/uniword/template/variable_resolver.rb +40 -7
  101. data/lib/uniword/themes/theme_transformation.rb +55 -41
  102. data/lib/uniword/toc/toc_generator.rb +2 -0
  103. data/lib/uniword/validation/engine.rb +35 -0
  104. data/lib/uniword/validation/link_checker.rb +1 -1
  105. data/lib/uniword/validation/opc_validator.rb +0 -1
  106. data/lib/uniword/validation/report/layer_result.rb +0 -1
  107. data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
  108. data/lib/uniword/validation/report/verification_report.rb +0 -2
  109. data/lib/uniword/validation/report.rb +20 -0
  110. data/lib/uniword/validation/rules/base.rb +10 -2
  111. data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
  112. data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
  113. data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
  114. data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
  115. data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
  116. data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
  117. data/lib/uniword/validation/rules/document_context.rb +7 -0
  118. data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
  119. data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
  120. data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
  121. data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
  122. data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
  123. data/lib/uniword/validation/rules/images_rule.rb +0 -2
  124. data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
  125. data/lib/uniword/validation/rules/model_context.rb +32 -0
  126. data/lib/uniword/validation/rules/model_rule.rb +34 -0
  127. data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
  128. data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
  129. data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
  130. data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
  131. data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
  132. data/lib/uniword/validation/rules/settings_rule.rb +0 -2
  133. data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
  134. data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
  135. data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
  136. data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
  137. data/lib/uniword/validation/rules/tables_rule.rb +0 -2
  138. data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
  139. data/lib/uniword/validation/rules/theme_rule.rb +0 -2
  140. data/lib/uniword/validation/rules.rb +61 -23
  141. data/lib/uniword/validation/schema_registry.rb +11 -0
  142. data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
  143. data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
  144. data/lib/uniword/validation/validators.rb +0 -9
  145. data/lib/uniword/validation/verify_orchestrator.rb +0 -7
  146. data/lib/uniword/validation.rb +7 -2
  147. data/lib/uniword/version.rb +1 -1
  148. data/lib/uniword/vml/imagedata.rb +0 -1
  149. data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
  150. data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
  151. data/lib/uniword/wordprocessingml/document_root.rb +211 -21
  152. data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
  153. data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
  154. data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
  155. data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
  156. data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
  157. data/lib/uniword/wordprocessingml/level.rb +6 -20
  158. data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
  159. data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
  160. data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
  161. data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
  162. data/lib/uniword/wordprocessingml/rsids.rb +0 -2
  163. data/lib/uniword/wordprocessingml/run.rb +3 -0
  164. data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
  165. data/lib/uniword/wordprocessingml/settings.rb +8 -34
  166. data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
  167. data/lib/uniword/wordprocessingml/style.rb +2 -1
  168. data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
  169. data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
  170. data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
  171. data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
  172. data/lib/uniword/wordprocessingml.rb +4 -0
  173. data/lib/uniword.rb +27 -2
  174. metadata +38 -19
  175. data/config/validation_rules.yml +0 -60
  176. data/config/warning_rules.yml +0 -57
  177. data/lib/uniword/validation/document_validator.rb +0 -272
  178. data/lib/uniword/validation/structural_validator.rb +0 -116
  179. data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
  180. data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
  181. data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
  182. data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
  183. data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
  184. data/lib/uniword/validators/element_validator.rb +0 -93
  185. data/lib/uniword/validators/paragraph_validator.rb +0 -116
  186. data/lib/uniword/validators/table_validator.rb +0 -134
  187. data/lib/uniword/validators.rb +0 -9
  188. data/lib/uniword/warnings/warning.rb +0 -130
  189. data/lib/uniword/warnings/warning_collector.rb +0 -234
  190. data/lib/uniword/warnings/warning_report.rb +0 -159
  191. data/lib/uniword/warnings.rb +0 -9
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Wordprocessingml
5
+ # Applies uniform page setup across every section of a document —
6
+ # Word's Layout dialog as an API.
7
+ #
8
+ # Paper size (named presets), orientation (with dimension swap, like
9
+ # Word), and margins (twips, or strings with in/cm/mm suffix).
10
+ # Header/footer/gutter margins are left untouched.
11
+ #
12
+ # @example A4 landscape with 2 cm margins
13
+ # setup = PageSetup.new(size: "a4", orientation: "landscape",
14
+ # margins: "2cm")
15
+ # setup.apply(document)
16
+ # setup.sections_updated # => 1
17
+ class PageSetup
18
+ # Named paper sizes in twips [width, height] (portrait).
19
+ PAPER_SIZES = {
20
+ "letter" => [12_240, 15_840],
21
+ "legal" => [12_240, 20_160],
22
+ "a4" => [11_906, 16_838],
23
+ "a5" => [8_391, 11_906],
24
+ "executive" => [10_440, 15_120],
25
+ }.freeze
26
+
27
+ TWIPS_PER_INCH = 1440
28
+ TWIPS_PER_CM = 567
29
+ TWIPS_PER_MM = 57
30
+
31
+ ORIENTATIONS = %w[portrait landscape].freeze
32
+
33
+ # @return [Integer] number of sections updated
34
+ attr_reader :sections_updated
35
+
36
+ # @param size [String, nil] Paper size name (see PAPER_SIZES)
37
+ # @param orientation [String, nil] "portrait" or "landscape"
38
+ # @param margins [Integer, String, nil] Uniform margin for all four
39
+ # sides (twips, or "1in" / "2.5cm" / "25mm")
40
+ # @param side_margins [Hash] Per-side overrides
41
+ # (:top, :right, :bottom, :left)
42
+ # @raise [ArgumentError] on unknown size, orientation, or margin
43
+ def initialize(size: nil, orientation: nil, margins: nil,
44
+ **side_margins)
45
+ @size = normalize_size(size)
46
+ @orientation = normalize_orientation(orientation)
47
+ @margins = side_margins.to_h do |side, value|
48
+ [side, parse_margin(value)]
49
+ end
50
+ uniform = parse_margin(margins)
51
+ %i[top right bottom left].each do |side|
52
+ @margins[side] = uniform if @margins[side].nil?
53
+ end
54
+ @sections_updated = 0
55
+ end
56
+
57
+ # Apply the setup to every section in the document.
58
+ #
59
+ # @param document [DocumentRoot] Document to update (mutated)
60
+ # @return [Integer] number of sections updated
61
+ def apply(document)
62
+ each_section_properties(document) do |sect_pr|
63
+ apply_page_size(sect_pr) if @size || @orientation
64
+ apply_margins(sect_pr)
65
+ @sections_updated += 1
66
+ end
67
+ sections_updated
68
+ end
69
+
70
+ private
71
+
72
+ def normalize_size(size)
73
+ return nil if size.nil?
74
+
75
+ dims = PAPER_SIZES[size.to_s.downcase]
76
+ unless dims
77
+ raise ArgumentError,
78
+ "Unknown paper size '#{size}'. " \
79
+ "Available: #{PAPER_SIZES.keys.join(', ')}"
80
+ end
81
+
82
+ dims
83
+ end
84
+
85
+ def normalize_orientation(orientation)
86
+ return nil if orientation.nil?
87
+
88
+ value = orientation.to_s.downcase
89
+ return value if ORIENTATIONS.include?(value)
90
+
91
+ raise ArgumentError,
92
+ "Unknown orientation '#{orientation}'. " \
93
+ "Available: #{ORIENTATIONS.join(', ')}"
94
+ end
95
+
96
+ # Parse a margin value: Integer (twips) or String with an
97
+ # in/cm/mm/dxa suffix (bare numbers are inches).
98
+ def parse_margin(value)
99
+ case value
100
+ when nil then nil
101
+ when Numeric then value.round
102
+ when /\A([\d.]+)\s*(in|cm|mm|dxa|twips?)?\z/i
103
+ amount = Regexp.last_match(1).to_f
104
+ unit = Regexp.last_match(2)&.downcase || "in"
105
+ to_twips(amount, unit)
106
+ else
107
+ raise ArgumentError, "Invalid margin '#{value}' " \
108
+ "(use twips, or '1in' / '2.5cm' / '25mm')"
109
+ end
110
+ end
111
+
112
+ def to_twips(amount, unit)
113
+ factor = case unit
114
+ when "in" then TWIPS_PER_INCH
115
+ when "cm" then TWIPS_PER_CM
116
+ when "mm" then TWIPS_PER_MM
117
+ else 1
118
+ end
119
+ (amount * factor).round
120
+ end
121
+
122
+ def each_section_properties(document, &block)
123
+ body = document.body
124
+ return unless body
125
+
126
+ yield body.section_properties if body.section_properties
127
+ each_paragraph_section(body, &block)
128
+ end
129
+
130
+ def each_paragraph_section(body)
131
+ (body.paragraphs || []).each do |paragraph|
132
+ sect_pr = paragraph.properties&.section_properties
133
+ yield sect_pr if sect_pr
134
+ end
135
+ end
136
+
137
+ def apply_page_size(sect_pr)
138
+ width, height = target_dimensions(sect_pr)
139
+
140
+ sect_pr.page_size ||= PageSize.new
141
+ sect_pr.page_size.width = width
142
+ sect_pr.page_size.height = height
143
+ sect_pr.page_size.orientation = @orientation if @orientation
144
+ end
145
+
146
+ def target_dimensions(sect_pr)
147
+ width, height = current_dimensions(sect_pr)
148
+ width, height = @size if @size
149
+ width, height = orient(width, height) if @orientation
150
+ [width, height]
151
+ end
152
+
153
+ def current_dimensions(sect_pr)
154
+ size = sect_pr.page_size
155
+ [size&.width || PageDefaults.default_page_size.width,
156
+ size&.height || PageDefaults.default_page_size.height]
157
+ end
158
+
159
+ # Word swaps width/height when flipping orientation.
160
+ def orient(width, height)
161
+ if (@orientation == "landscape" && width < height) ||
162
+ (@orientation == "portrait" && width > height)
163
+ [height, width]
164
+ else
165
+ [width, height]
166
+ end
167
+ end
168
+
169
+ def apply_margins(sect_pr)
170
+ return if @margins.values.all?(&:nil?)
171
+
172
+ sect_pr.page_margins ||= PageMargins.new
173
+ @margins.each do |side, value|
174
+ next if value.nil?
175
+
176
+ sect_pr.page_margins.method(:"#{side}=").call(value)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -116,7 +116,8 @@ module Uniword
116
116
  with: { from: :yaml_widow_control_from, to: :yaml_widow_control_to }
117
117
  end
118
118
 
119
- # YAML transform methods (instance methods - called via send on an instance)
119
+ # YAML transform methods (instance methods called by lutaml-model's
120
+ # `with:` transform mechanism)
120
121
  def yaml_style_from(instance, value)
121
122
  if value
122
123
  instance.style = [
@@ -200,21 +201,6 @@ module Uniword
200
201
  # Style reference - maps w:pStyle w:val="..." to style attribute
201
202
  map_element "pStyle", to: :style, render_nil: false
202
203
 
203
- # Alignment - maps w:jc w:val="..." to alignment attribute
204
- map_element "jc", to: :alignment, render_nil: false
205
-
206
- # Spacing (complex object - WORKS)
207
- map_element "spacing", to: :spacing, render_nil: false
208
-
209
- # Indentation (complex object)
210
- map_element "ind", to: :indentation, render_nil: false
211
-
212
- # Outline level (wrapper object)
213
- map_element "outlineLvl", to: :outline_level, render_nil: false
214
-
215
- # Numbering properties (wrapped in w:numPr)
216
- map_element "numPr", to: :numbering_properties, render_nil: false
217
-
218
204
  # Keep options (only render if true)
219
205
  map_element "keepNext", to: :keep_next_wrapper, render_nil: false,
220
206
  render_default: false
@@ -225,32 +211,51 @@ module Uniword
225
211
  map_element "widowControl", to: :widow_control_wrapper, render_nil: false,
226
212
  render_default: false
227
213
 
228
- # Spacing options (only render if true)
229
- map_element "contextualSpacing", to: :contextual_spacing, render_nil: false,
230
- render_default: false
214
+ # Numbering properties (wrapped in w:numPr)
215
+ map_element "numPr", to: :numbering_properties, render_nil: false
216
+
217
+ # Suppress line numbers (only render if true)
231
218
  map_element "suppressLineNumbers", to: :suppress_line_numbers, render_nil: false,
232
219
  render_default: false
233
220
 
234
- # Bidirectional (only render if true)
235
- map_element "bidi", to: :bidirectional, render_nil: false,
236
- render_default: false
221
+ # Borders (complex object)
222
+ map_element "pBdr", to: :borders, render_nil: false
223
+
224
+ # Shading (complex object)
225
+ map_element "shd", to: :shading, render_nil: false
226
+
227
+ # Tabs (complex object - collection)
228
+ map_element "tabs", to: :tabs, render_nil: false
237
229
 
238
230
  # East Asian typography
239
231
  map_element "autoSpaceDE", to: :auto_space_de, render_nil: false,
240
232
  render_default: false
241
233
  map_element "autoSpaceDN", to: :auto_space_dn, render_nil: false,
242
234
  render_default: false
235
+
236
+ # Bidirectional (only render if true)
237
+ map_element "bidi", to: :bidirectional, render_nil: false,
238
+ render_default: false
239
+
240
+ # Right indent adjustment
243
241
  map_element "adjustRightInd", to: :adjust_right_ind, render_nil: false,
244
242
  render_default: false
245
243
 
246
- # Borders (complex object)
247
- map_element "pBdr", to: :borders, render_nil: false
244
+ # Spacing (complex object - WORKS)
245
+ map_element "spacing", to: :spacing, render_nil: false
248
246
 
249
- # Tabs (complex object - collection)
250
- map_element "tabs", to: :tabs, render_nil: false
247
+ # Indentation (complex object)
248
+ map_element "ind", to: :indentation, render_nil: false
251
249
 
252
- # Shading (complex object)
253
- map_element "shd", to: :shading, render_nil: false
250
+ # Contextual spacing (only render if true)
251
+ map_element "contextualSpacing", to: :contextual_spacing, render_nil: false,
252
+ render_default: false
253
+
254
+ # Alignment - maps w:jc w:val="..." to alignment attribute
255
+ map_element "jc", to: :alignment, render_nil: false
256
+
257
+ # Outline level (wrapper object)
258
+ map_element "outlineLvl", to: :outline_level, render_nil: false
254
259
 
255
260
  # Run properties (for pPr/rPr - style overrides)
256
261
  map_element "rPr", to: :run_properties, render_nil: false
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "rsid_root"
5
- require_relative "rsid"
6
4
 
7
5
  module Uniword
8
6
  module Wordprocessingml
@@ -18,6 +18,7 @@ module Uniword
18
18
  attribute :alternate_content, AlternateContent, default: nil
19
19
  attribute :footnote_reference, FootnoteReference
20
20
  attribute :endnote_reference, EndnoteReference
21
+ attribute :comment_reference, CommentReference
21
22
  attribute :field_char, FieldChar
22
23
  attribute :instr_text, InstrText
23
24
  attribute :position_tab, PositionTab
@@ -55,6 +56,8 @@ module Uniword
55
56
  render_nil: false
56
57
  map_element "endnoteReference", to: :endnote_reference,
57
58
  render_nil: false
59
+ map_element "commentReference", to: :comment_reference,
60
+ render_nil: false
58
61
  map_element "fldChar", to: :field_char, render_nil: false
59
62
  map_element "instrText", to: :instr_text, render_nil: false
60
63
  map_element "ptab", to: :position_tab, render_nil: false
@@ -50,14 +50,14 @@ module Uniword
50
50
 
51
51
  map_element "headerReference", to: :header_references, render_nil: false
52
52
  map_element "footerReference", to: :footer_references, render_nil: false
53
+ map_element "footnotePr", to: :footnote_pr, render_nil: false
53
54
  map_element "pgSz", to: :page_size, render_nil: false
54
55
  map_element "pgMar", to: :page_margins, render_nil: false
55
56
  map_attribute "val", to: :type
56
57
  map_element "pgNumType", to: :page_numbering, render_nil: false
57
58
  map_element "cols", to: :columns, render_nil: false
58
- map_element "docGrid", to: :doc_grid, render_nil: false
59
- map_element "footnotePr", to: :footnote_pr, render_nil: false
60
59
  map_element "titlePg", to: :title_pg, render_nil: false
60
+ map_element "docGrid", to: :doc_grid, render_nil: false
61
61
  map_element "footnoteColumns", to: :footnote_columns, render_nil: false
62
62
  end
63
63
  end
@@ -2,34 +2,6 @@
2
2
 
3
3
  require "lutaml/model"
4
4
  require "nokogiri"
5
- require_relative "../properties/relationship_id"
6
- require_relative "zoom"
7
- require_relative "compat"
8
- require_relative "proof_state"
9
- require_relative "style_pane_format_filter"
10
- require_relative "default_tab_stop"
11
- require_relative "character_spacing_control"
12
- require_relative "do_not_display_page_boundaries"
13
- require_relative "rsids"
14
- require_relative "math_pr"
15
- require_relative "theme_font_lang"
16
- require_relative "clr_scheme_mapping"
17
- require_relative "shape_defaults"
18
- require_relative "decimal_symbol"
19
- require_relative "list_separator"
20
- require_relative "attached_template"
21
- require_relative "footnote_pr"
22
- require_relative "endnote_pr"
23
- require_relative "hdr_shape_defaults"
24
- require_relative "even_and_odd_headers"
25
- require_relative "mirror_margins"
26
- require_relative "do_not_include_subdocs_in_stats"
27
- require_relative "hyphenation_zone"
28
- require_relative "style_pane_sort_method"
29
- require_relative "doc_vars"
30
- require_relative "w14_doc_id"
31
- require_relative "w15_chart_tracking_ref_based"
32
- require_relative "w15_doc_id"
33
5
 
34
6
  module Uniword
35
7
  module Wordprocessingml
@@ -57,6 +29,7 @@ module Uniword
57
29
  attribute :footnote_pr, FootnotePr
58
30
  attribute :endnote_pr, EndnotePr
59
31
  attribute :hdr_shape_defaults, HdrShapeDefaults
32
+ attribute :update_fields, UpdateFields
60
33
  attribute :even_and_odd_headers, EvenAndOddHeaders
61
34
  attribute :mirror_margins, MirrorMargins
62
35
  attribute :do_not_include_subdocs_in_stats,
@@ -112,9 +85,9 @@ module Uniword
112
85
  map_element "doNotDisplayPageBoundaries",
113
86
  to: :do_not_display_page_boundaries, render_nil: false
114
87
  map_element "mirrorMargins", to: :mirror_margins, render_nil: false
88
+ map_element "proofState", to: :proof_state, render_nil: false
115
89
  map_element "attachedTemplate", to: :attached_template,
116
90
  render_nil: false
117
- map_element "proofState", to: :proof_state, render_nil: false
118
91
  map_element "stylePaneFormatFilter", to: :style_pane_format_filter,
119
92
  render_nil: false
120
93
  map_element "stylePaneSortMethod", to: :style_pane_sort_method,
@@ -126,6 +99,9 @@ module Uniword
126
99
  render_nil: false
127
100
  map_element "characterSpacingControl", to: :character_spacing_control,
128
101
  render_nil: false
102
+ map_element "updateFields", to: :update_fields, render_nil: false
103
+ map_element "hdrShapeDefaults", to: :hdr_shape_defaults,
104
+ render_nil: false
129
105
  map_element "footnotePr", to: :footnote_pr, render_nil: false
130
106
  map_element "endnotePr", to: :endnote_pr, render_nil: false
131
107
  map_element "compat", to: :compat, render_nil: false
@@ -137,11 +113,6 @@ module Uniword
137
113
  render_nil: false
138
114
  map_element "doNotIncludeSubdocsInStats",
139
115
  to: :do_not_include_subdocs_in_stats, render_nil: false
140
- map_element "hdrShapeDefaults", to: :hdr_shape_defaults,
141
- render_nil: false
142
- map_element "shapeDefaults", to: :shape_defaults, render_nil: false
143
- map_element "decimalSymbol", to: :decimal_symbol, render_nil: false
144
- map_element "listSeparator", to: :list_separator, render_nil: false
145
116
  # Both w14:docId and w15:docId use the same element name 'docId'
146
117
  # Separate map_element entries needed for namespace-aware matching
147
118
  # The namespace_scope ensures w14 and w15 namespaces are declared on root
@@ -150,6 +121,9 @@ module Uniword
150
121
  map_element "docId", to: :w14_doc_id, render_nil: false
151
122
  map_element "docId", to: :w15_doc_id, render_nil: false
152
123
  map_element "schemaLibrary", to: :schema_library, render_nil: false
124
+ map_element "shapeDefaults", to: :shape_defaults, render_nil: false
125
+ map_element "decimalSymbol", to: :decimal_symbol, render_nil: false
126
+ map_element "listSeparator", to: :list_separator, render_nil: false
153
127
  end
154
128
 
155
129
  # Override from_xml to manually deserialize w15:docId which has the same
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "../vml_office"
5
4
 
6
5
  module Uniword
7
6
  module Wordprocessingml
@@ -49,7 +49,8 @@ module Uniword
49
49
  map "run_properties", to: :rPr
50
50
  end
51
51
 
52
- # YAML transform methods (instance methods - called via send on an instance)
52
+ # YAML transform methods (instance methods called by lutaml-model's
53
+ # `with:` transform mechanism)
53
54
  def yaml_name_from(instance, value)
54
55
  instance.name = StyleName.new(val: value) if value
55
56
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Wordprocessingml
5
+ # Removes styles from a document's style definitions — Word's
6
+ # Styles-pane management as an API.
7
+ #
8
+ # `#remove_unused` deletes styles that no content references,
9
+ # directly (pStyle/rStyle/tblStyle in body, headers, footers, notes,
10
+ # comments) or transitively (basedOn/link/next chains from a used
11
+ # style, numbering level pStyle). Default styles (w:default="1")
12
+ # are never removed.
13
+ #
14
+ # @example Declutter a document
15
+ # cleanup = StyleCleanup.new(document)
16
+ # removed = cleanup.remove_unused # => ["ObsoleteStyle", ...]
17
+ class StyleCleanup
18
+ # @param document [DocumentRoot] Document to clean (mutated)
19
+ def initialize(document)
20
+ @document = document
21
+ end
22
+
23
+ # Remove one style by id (unless protected).
24
+ #
25
+ # @param style_id [String] Style id (w:styleId)
26
+ # @return [Boolean] true when the style was removed
27
+ def remove?(style_id)
28
+ return false if protected_id?(style_id)
29
+
30
+ before = styles.size
31
+ styles.delete_if { |style| style.styleId == style_id }
32
+ styles.size < before
33
+ end
34
+
35
+ # Ids of styles no content references.
36
+ #
37
+ # @return [Array<String>] Unreferenced, unprotected style ids
38
+ def unused_ids
39
+ used = used_ids
40
+ styles.filter_map(&:styleId).uniq - used - protected_ids
41
+ end
42
+
43
+ # Remove every unreferenced style.
44
+ #
45
+ # @return [Array<String>] Ids of removed styles
46
+ def remove_unused
47
+ ids = unused_ids
48
+ ids.each { |id| remove?(id) }
49
+ ids
50
+ end
51
+
52
+ private
53
+
54
+ def styles
55
+ @document.styles_configuration.styles
56
+ end
57
+
58
+ def protected_ids
59
+ styles.select(&:default).map(&:styleId)
60
+ end
61
+
62
+ def protected_id?(style_id)
63
+ protected_ids.include?(style_id)
64
+ end
65
+
66
+ # Ids referenced anywhere: content, notes, headers/footers,
67
+ # comments, numbering levels, and style-to-style chains.
68
+ def used_ids
69
+ used = content_style_ids + chain_seed_ids
70
+ expand_chains(used)
71
+ end
72
+
73
+ def content_style_ids
74
+ ids = []
75
+ walk_all_paragraphs do |paragraph|
76
+ ids.concat(paragraph_style_ids(paragraph))
77
+ ids.concat(run_style_ids(paragraph))
78
+ end
79
+ ids.concat(table_style_ids)
80
+ ids.concat(numbering_style_ids)
81
+ ids
82
+ end
83
+
84
+ # Styles referenced by other styles (basedOn/link/next) — chains
85
+ # are expanded from these too.
86
+ def chain_seed_ids
87
+ styles.flat_map { |style| chain_links(style) }
88
+ end
89
+
90
+ def chain_links(style)
91
+ [style.basedOn&.val, style.link&.val, style.nextStyle&.val].compact
92
+ end
93
+
94
+ def expand_chains(ids)
95
+ expanded = ids.to_set
96
+ loop do
97
+ additions = chain_additions(expanded)
98
+ break if additions.empty?
99
+
100
+ additions.each { |id| expanded << id }
101
+ end
102
+ expanded.to_a
103
+ end
104
+
105
+ def chain_additions(expanded)
106
+ styles.select { |style| expanded.include?(style.styleId) }
107
+ .flat_map { |style| chain_links(style) }
108
+ .reject { |id| expanded.include?(id) }
109
+ end
110
+
111
+ def paragraph_style_ids(paragraph)
112
+ Array(paragraph.properties&.style).filter_map(&:value)
113
+ end
114
+
115
+ def run_style_ids(paragraph)
116
+ paragraph_runs(paragraph)
117
+ .filter_map { |run| run.properties&.style&.value }
118
+ end
119
+
120
+ def paragraph_runs(paragraph)
121
+ (paragraph.runs || []) +
122
+ (paragraph.hyperlinks || []).flat_map(&:runs)
123
+ end
124
+
125
+ def table_style_ids
126
+ ids = []
127
+ walk_tables(@document.body) do |table|
128
+ ids << table.properties&.style&.val
129
+ end
130
+ ids.compact
131
+ end
132
+
133
+ def numbering_style_ids
134
+ numbering_definitions.flat_map do |definition|
135
+ (definition.levels || []).filter_map { |level| level.pStyle&.val }
136
+ end
137
+ end
138
+
139
+ def numbering_definitions
140
+ @document.numbering_configuration&.definitions || []
141
+ end
142
+
143
+ # Paragraphs in body (incl. table cells), headers/footers,
144
+ # notes, comments.
145
+ def walk_all_paragraphs(&block)
146
+ each_container { |container| walk_paragraphs(container, &block) }
147
+ end
148
+
149
+ def walk_paragraphs(container, &block)
150
+ return unless container
151
+
152
+ (container.paragraphs || []).each(&block)
153
+ walk_tables(container) { |table| walk_table_rows(table, &block) }
154
+ end
155
+
156
+ def walk_table_rows(table, &block)
157
+ (table.rows || []).each do |row|
158
+ (row.cells || []).each { |cell| walk_paragraphs(cell, &block) }
159
+ end
160
+ end
161
+
162
+ def walk_tables(container, &block)
163
+ return unless container
164
+
165
+ (container.tables || []).each do |table|
166
+ yield table
167
+ walk_table_cells(table, &block)
168
+ end
169
+ end
170
+
171
+ def walk_table_cells(table, &block)
172
+ (table.rows || []).each do |row|
173
+ (row.cells || []).each { |cell| walk_tables(cell, &block) }
174
+ end
175
+ end
176
+
177
+ def each_container(&block)
178
+ yield @document.body if @document.body
179
+ header_footer_containers.each(&block)
180
+ note_containers.each(&block)
181
+ end
182
+
183
+ def header_footer_containers
184
+ (@document.header_footer_parts || []).filter_map do |part|
185
+ part.content if part.content.is_a?(Lutaml::Model::Serializable)
186
+ end
187
+ end
188
+
189
+ def note_containers
190
+ [
191
+ @document.footnotes&.footnote_entries,
192
+ @document.endnotes&.endnote_entries,
193
+ @document.comments&.comments,
194
+ ].flatten.compact
195
+ end
196
+ end
197
+ end
198
+ end
@@ -28,18 +28,18 @@ module Uniword
28
28
  namespace Uniword::Ooxml::Namespaces::WordProcessingML
29
29
  mixed_content
30
30
 
31
+ map_element "cnfStyle", to: :cnf_style, render_nil: false
31
32
  map_element "tcW", to: :cell_width, render_nil: false
32
- map_element "tcBorders", to: :borders, render_nil: false
33
- map_element "shd", to: :shading, render_nil: false
34
- map_element "vAlign", to: :vertical_align, render_nil: false
35
33
  map_element "gridSpan", to: :grid_span, render_nil: false
36
34
  map_element "vMerge", to: :v_merge, render_nil: false
37
- map_element "tcMar", to: :tc_mar, render_nil: false
38
- map_element "cnfStyle", to: :cnf_style, render_nil: false
35
+ map_element "tcBorders", to: :borders, render_nil: false
36
+ map_element "shd", to: :shading, render_nil: false
39
37
  map_element "noWrap", to: :no_wrap, render_nil: false
40
- map_element "hideMark", to: :hide_mark, render_nil: false
38
+ map_element "tcMar", to: :tc_mar, render_nil: false
41
39
  map_element "textDirection", to: :text_direction,
42
40
  render_nil: false
41
+ map_element "vAlign", to: :vertical_align, render_nil: false
42
+ map_element "hideMark", to: :hide_mark, render_nil: false
43
43
  end
44
44
  end
45
45
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Uniword
6
+ module Wordprocessingml
7
+ # Update fields when the document is opened
8
+ #
9
+ # Element: <w:updateFields>
10
+ #
11
+ # When true, Word updates all fields (TOC, page numbers, cross
12
+ # references) the first time the document is opened — generated
13
+ # documents show correct field values without a manual F9.
14
+ class UpdateFields < Lutaml::Model::Serializable
15
+ attribute :value, Ooxml::Types::OoxmlBoolean,
16
+ default: -> { true }
17
+
18
+ xml do
19
+ element "updateFields"
20
+ namespace Uniword::Ooxml::Namespaces::WordProcessingML
21
+
22
+ map_attribute "val", to: :value, render_nil: false,
23
+ render_default: false
24
+ end
25
+ end
26
+ end
27
+ end