uniword 1.2.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +313 -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 +22 -14
  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 +11 -10
  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 +12 -18
  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 +91 -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/image_part.rb +83 -0
  38. data/lib/uniword/docx/package.rb +141 -333
  39. data/lib/uniword/docx/package_defaults.rb +83 -99
  40. data/lib/uniword/docx/package_integrity_checker.rb +312 -0
  41. data/lib/uniword/docx/package_serialization.rb +144 -233
  42. data/lib/uniword/docx/part.rb +125 -0
  43. data/lib/uniword/docx/part_collection.rb +131 -0
  44. data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
  45. data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
  46. data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
  47. data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
  48. data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
  49. data/lib/uniword/docx/part_loader/load_context.rb +85 -0
  50. data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
  51. data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
  52. data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
  53. data/lib/uniword/docx/part_loader.rb +92 -0
  54. data/lib/uniword/docx/raw_part.rb +75 -0
  55. data/lib/uniword/docx/reconciler/body.rb +59 -68
  56. data/lib/uniword/docx/reconciler/fix.rb +46 -0
  57. data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
  58. data/lib/uniword/docx/reconciler/helpers.rb +18 -31
  59. data/lib/uniword/docx/reconciler/notes.rb +17 -8
  60. data/lib/uniword/docx/reconciler/package_structure.rb +168 -221
  61. data/lib/uniword/docx/reconciler/parts.rb +36 -13
  62. data/lib/uniword/docx/reconciler/referential_integrity.rb +222 -123
  63. data/lib/uniword/docx/reconciler/tables.rb +78 -16
  64. data/lib/uniword/docx/reconciler/theme.rb +7 -4
  65. data/lib/uniword/docx/reconciler.rb +36 -14
  66. data/lib/uniword/docx.rb +13 -0
  67. data/lib/uniword/drawingml/blip.rb +0 -1
  68. data/lib/uniword/drawingml/color_scheme.rb +5 -6
  69. data/lib/uniword/drawingml/font_scheme.rb +4 -8
  70. data/lib/uniword/drawingml/theme.rb +12 -6
  71. data/lib/uniword/errors.rb +8 -1
  72. data/lib/uniword/footer.rb +4 -0
  73. data/lib/uniword/header.rb +4 -0
  74. data/lib/uniword/hyperlink.rb +1 -1
  75. data/lib/uniword/image.rb +6 -6
  76. data/lib/uniword/images/image_manager.rb +13 -13
  77. data/lib/uniword/lazy_loader.rb +1 -2
  78. data/lib/uniword/model_attribute_access.rb +20 -4
  79. data/lib/uniword/ooxml/dotx_package.rb +42 -2
  80. data/lib/uniword/ooxml/element_order.rb +94 -0
  81. data/lib/uniword/ooxml/part_definition.rb +329 -0
  82. data/lib/uniword/ooxml/part_registry.rb +504 -0
  83. data/lib/uniword/ooxml/relationships/image_relationship.rb +6 -3
  84. data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
  85. data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
  86. data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
  87. data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
  88. data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
  89. data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
  90. data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
  91. data/lib/uniword/ooxml/types.rb +10 -0
  92. data/lib/uniword/ooxml.rb +3 -0
  93. data/lib/uniword/properties/alignment.rb +8 -3
  94. data/lib/uniword/properties/border.rb +52 -6
  95. data/lib/uniword/properties/cell_vertical_align.rb +4 -1
  96. data/lib/uniword/properties/color_value.rb +8 -3
  97. data/lib/uniword/properties/highlight.rb +9 -5
  98. data/lib/uniword/properties/shading.rb +4 -3
  99. data/lib/uniword/properties/tab_stop.rb +6 -1
  100. data/lib/uniword/properties/table_justification.rb +4 -1
  101. data/lib/uniword/properties/underline.rb +4 -2
  102. data/lib/uniword/properties/vertical_align.rb +6 -3
  103. data/lib/uniword/review/review_manager.rb +23 -9
  104. data/lib/uniword/revision.rb +9 -9
  105. data/lib/uniword/schema/model_generator.rb +31 -15
  106. data/lib/uniword/shared_types/hex_color.rb +4 -1
  107. data/lib/uniword/shared_types/pixel_measure.rb +4 -1
  108. data/lib/uniword/shared_types/point_measure.rb +4 -1
  109. data/lib/uniword/shared_types/text_alignment.rb +6 -1
  110. data/lib/uniword/shared_types/twips_measure.rb +4 -1
  111. data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
  112. data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
  113. data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
  114. data/lib/uniword/template/variable_resolver.rb +40 -7
  115. data/lib/uniword/themes/theme_transformation.rb +55 -41
  116. data/lib/uniword/toc/toc_generator.rb +6 -5
  117. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
  118. data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
  119. data/lib/uniword/validation/engine.rb +35 -0
  120. data/lib/uniword/validation/link_checker.rb +1 -1
  121. data/lib/uniword/validation/opc_validator.rb +16 -2
  122. data/lib/uniword/validation/report/layer_result.rb +0 -1
  123. data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
  124. data/lib/uniword/validation/report/verification_report.rb +0 -2
  125. data/lib/uniword/validation/report.rb +20 -0
  126. data/lib/uniword/validation/rules/base.rb +10 -2
  127. data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
  128. data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
  129. data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
  130. data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
  131. data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
  132. data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
  133. data/lib/uniword/validation/rules/document_context.rb +7 -0
  134. data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
  135. data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
  136. data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
  137. data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
  138. data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
  139. data/lib/uniword/validation/rules/images_rule.rb +0 -2
  140. data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
  141. data/lib/uniword/validation/rules/model_context.rb +32 -0
  142. data/lib/uniword/validation/rules/model_rule.rb +34 -0
  143. data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
  144. data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
  145. data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
  146. data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
  147. data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
  148. data/lib/uniword/validation/rules/settings_rule.rb +0 -2
  149. data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
  150. data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
  151. data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
  152. data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
  153. data/lib/uniword/validation/rules/tables_rule.rb +0 -2
  154. data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
  155. data/lib/uniword/validation/rules/theme_rule.rb +0 -2
  156. data/lib/uniword/validation/rules.rb +61 -23
  157. data/lib/uniword/validation/schema_registry.rb +11 -0
  158. data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
  159. data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -2
  160. data/lib/uniword/validation/validators.rb +0 -9
  161. data/lib/uniword/validation/verify_orchestrator.rb +0 -7
  162. data/lib/uniword/validation.rb +7 -2
  163. data/lib/uniword/version.rb +1 -1
  164. data/lib/uniword/vml/imagedata.rb +0 -1
  165. data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
  166. data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
  167. data/lib/uniword/wordprocessingml/document_root.rb +121 -122
  168. data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
  169. data/lib/uniword/wordprocessingml/endnotes.rb +4 -1
  170. data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
  171. data/lib/uniword/wordprocessingml/footnotes.rb +4 -1
  172. data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
  173. data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
  174. data/lib/uniword/wordprocessingml/level.rb +6 -20
  175. data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
  176. data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
  177. data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
  178. data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
  179. data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
  180. data/lib/uniword/wordprocessingml/rsids.rb +0 -2
  181. data/lib/uniword/wordprocessingml/run.rb +3 -0
  182. data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
  183. data/lib/uniword/wordprocessingml/settings.rb +8 -34
  184. data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
  185. data/lib/uniword/wordprocessingml/style.rb +2 -1
  186. data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
  187. data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
  188. data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
  189. data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
  190. data/lib/uniword/wordprocessingml.rb +5 -2
  191. data/lib/uniword.rb +27 -2
  192. metadata +52 -19
  193. data/config/validation_rules.yml +0 -60
  194. data/config/warning_rules.yml +0 -57
  195. data/lib/uniword/validation/document_validator.rb +0 -272
  196. data/lib/uniword/validation/structural_validator.rb +0 -116
  197. data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
  198. data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
  199. data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
  200. data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
  201. data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
  202. data/lib/uniword/validators/element_validator.rb +0 -93
  203. data/lib/uniword/validators/paragraph_validator.rb +0 -116
  204. data/lib/uniword/validators/table_validator.rb +0 -134
  205. data/lib/uniword/validators.rb +0 -9
  206. data/lib/uniword/warnings/warning.rb +0 -130
  207. data/lib/uniword/warnings/warning_collector.rb +0 -234
  208. data/lib/uniword/warnings/warning_report.rb +0 -159
  209. data/lib/uniword/warnings.rb +0 -9
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Ooxml
5
+ # Declarative description of one OOXML package part kind.
6
+ #
7
+ # A PartDefinition is the single source of truth for a part's
8
+ # package path, content type, relationship type, the form its
9
+ # [Content_Types].xml entry takes (Default by extension or Override
10
+ # by part name), how it is loaded from extracted ZIP content, and
11
+ # where it lives on both Wordprocessingml::DocumentRoot and
12
+ # Docx::Package. Instances are immutable value objects registered
13
+ # in {PartRegistry}; consumers derive every literal from them.
14
+ #
15
+ # Parts with instance-numbered paths (headers, footers, charts,
16
+ # customXml items) carry a +path_pattern+/+target_pattern+ with
17
+ # +format+-style placeholders (e.g. "word/header%<counter>d.xml")
18
+ # instead of a fixed path.
19
+ #
20
+ # Load behavior beyond this data (parse rules for special part
21
+ # kinds) lives in named Docx::PartLoader strategy classes selected
22
+ # by +loader+; the definition itself stays a value object.
23
+ #
24
+ # @example Fixed part
25
+ # PartDefinition.new(
26
+ # key: :styles, path: "word/styles.xml", target: "styles.xml",
27
+ # content_type: "...styles+xml",
28
+ # rel_type: ".../relationships/styles",
29
+ # required: true, kind: :override, rels_scope: :document,
30
+ # loader: :xml_model,
31
+ # loader_model: Uniword::Wordprocessingml::StylesConfiguration,
32
+ # load_priority: 50,
33
+ # package_attribute: :styles,
34
+ # document_attribute: :styles_configuration,
35
+ # )
36
+ class PartDefinition
37
+ # @return [Symbol] unique registry key (e.g. :styles)
38
+ attr_reader :key
39
+
40
+ # @return [String, nil] package-relative path ("word/styles.xml")
41
+ attr_reader :path
42
+
43
+ # @return [String, nil] path template for instance-numbered parts
44
+ attr_reader :path_pattern
45
+
46
+ # @return [String, nil] relationship Target as written in .rels
47
+ # (relative to the owning part; defaults to +path+)
48
+ attr_reader :target
49
+
50
+ # @return [String, nil] Target template for numbered parts
51
+ attr_reader :target_pattern
52
+
53
+ # @return [String, nil] MIME content type (nil when dynamic,
54
+ # e.g. images whose type depends on the file extension)
55
+ attr_reader :content_type
56
+
57
+ # @return [String, nil] relationship type URI (nil when the part
58
+ # is not referenced by a relationship)
59
+ attr_reader :rel_type
60
+
61
+ # @return [String, nil] file extension for Default entries
62
+ attr_reader :extension
63
+
64
+ # @return [Boolean] whether a minimal valid package carries it
65
+ attr_reader :required
66
+
67
+ # @return [Symbol] :override, :default, or :none (no
68
+ # [Content_Types].xml entry of its own)
69
+ attr_reader :kind
70
+
71
+ # @return [Symbol, nil] :package or :document — which .rels part
72
+ # the relationship lives in (nil when +rel_type+ is nil)
73
+ attr_reader :rels_scope
74
+
75
+ # @return [Boolean] whether ContentTypes.generate includes it in
76
+ # the comprehensive [Content_Types].xml for new DOCX packages
77
+ attr_reader :standard
78
+
79
+ # @return [Symbol, nil] Docx::PartLoader strategy key
80
+ # (:xml_model, :custom_xml, :header_footer, :chart, :image,
81
+ # :embedding, :theme_media); nil when the part is not loaded
82
+ # from ZIP content
83
+ attr_reader :loader
84
+
85
+ # @return [Class, nil] model class parsing the part's XML
86
+ # (responds to +.from_xml+); used by loaders that parse
87
+ attr_reader :loader_model
88
+
89
+ # @return [Symbol, nil] dynamic path resolution rule:
90
+ # :office_document (resolve the main document path from the
91
+ # package-level officeDocument relationship) or
92
+ # :office_document_rels (sidecar .rels of the resolved main
93
+ # document); nil for fixed paths
94
+ attr_reader :path_resolution
95
+
96
+ # @return [Integer, nil] load ordering weight — lower loads
97
+ # first; entries sharing a priority load in registration order
98
+ attr_reader :load_priority
99
+
100
+ # @return [Symbol, nil] attribute (reader/writer) holding this
101
+ # part on Wordprocessingml::DocumentRoot
102
+ attr_reader :document_attribute
103
+
104
+ # @return [Symbol, nil] attribute (reader/writer) holding this
105
+ # part on Docx::Package
106
+ attr_reader :package_attribute
107
+
108
+ # @return [Boolean] whether the package→document copy includes
109
+ # this part (false when the loader already places the part on
110
+ # the document, or the part is never read from ZIP content)
111
+ attr_reader :copy_to_document
112
+
113
+ # @return [Symbol, nil] predicate on the document guarding the
114
+ # document→package copy (e.g. :numbering_configuration_loaded?
115
+ # for the lazily-initialized numbering configuration)
116
+ attr_reader :to_package_guard
117
+
118
+ # @return [Class, nil] class the value must be an instance of
119
+ # for the document→package copy (e.g. CommentsPart, since
120
+ # DocumentRoot#comments can hold non-part values)
121
+ attr_reader :to_package_type
122
+
123
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
124
+ # rubocop:disable Metrics/AbcSize
125
+ def initialize(key:, kind:, path: nil, path_pattern: nil,
126
+ target: nil, target_pattern: nil,
127
+ content_type: nil, rel_type: nil, extension: nil,
128
+ required: false, rels_scope: nil, standard: false,
129
+ loader: nil, loader_model: nil, path_resolution: nil,
130
+ load_priority: nil, document_attribute: nil,
131
+ package_attribute: nil, copy_to_document: true,
132
+ to_package_guard: nil, to_package_type: nil)
133
+ @key = key.to_sym
134
+ @kind = kind
135
+ @path = path
136
+ @path_pattern = path_pattern
137
+ @target = target || path
138
+ @target_pattern = target_pattern
139
+ @content_type = content_type
140
+ @rel_type = rel_type
141
+ @extension = extension
142
+ @required = required
143
+ @rels_scope = rels_scope
144
+ @standard = standard
145
+ @loader = loader
146
+ @loader_model = loader_model
147
+ @path_resolution = path_resolution
148
+ @load_priority = load_priority
149
+ @document_attribute = document_attribute
150
+ @package_attribute = package_attribute
151
+ @copy_to_document = copy_to_document
152
+ @to_package_guard = to_package_guard
153
+ @to_package_type = to_package_type
154
+ end
155
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
156
+ # rubocop:enable Metrics/AbcSize
157
+
158
+ # @return [Boolean] true for [Content_Types].xml Override entries
159
+ def override?
160
+ kind == :override
161
+ end
162
+
163
+ # @return [Boolean] true for [Content_Types].xml Default entries
164
+ def default?
165
+ kind == :default
166
+ end
167
+
168
+ # @return [Boolean] true when a minimal valid package carries it
169
+ def required?
170
+ !!required
171
+ end
172
+
173
+ # @return [Boolean] true when ContentTypes.generate includes it
174
+ def standard?
175
+ !!standard
176
+ end
177
+
178
+ # @return [Boolean] true when the relationship lives in _rels/.rels
179
+ def package_rel?
180
+ rels_scope == :package
181
+ end
182
+
183
+ # @return [Boolean] true when the part loads from ZIP content
184
+ def loadable?
185
+ !loader.nil?
186
+ end
187
+
188
+ # @return [Boolean] true when the package→document copy mirrors
189
+ # this part onto Wordprocessingml::DocumentRoot
190
+ def copy_to_document?
191
+ !!copy_to_document
192
+ end
193
+
194
+ # Whether a package path matches this definition's fixed path or
195
+ # path pattern. Accepts both "word/styles.xml" and
196
+ # "/word/styles.xml" forms, and matches numbered paths against
197
+ # the pattern ("word/header2.xml" → :header).
198
+ #
199
+ # @param candidate [String] package-relative path
200
+ # @return [Boolean]
201
+ def match_path?(candidate)
202
+ normalized = candidate.to_s.delete_prefix("/")
203
+ return true if path == normalized
204
+ return false unless path_pattern
205
+
206
+ normalized.match?(template_regex(path_pattern))
207
+ end
208
+
209
+ # Static prefix of the path pattern up to the first placeholder
210
+ # (e.g. "word/theme/media/" for "word/theme/media/%<name>s").
211
+ #
212
+ # @return [String, nil] nil for fixed-path definitions
213
+ def pattern_prefix
214
+ path_pattern&.split(/%<\w+>[ds]/)&.first
215
+ end
216
+
217
+ # Resolve the package-relative path, interpolating pattern
218
+ # placeholders for instance-numbered parts.
219
+ #
220
+ # @param vars [Hash] placeholder values (e.g. counter: 1)
221
+ # @return [String, nil] e.g. "word/header1.xml"
222
+ def path_for(**vars)
223
+ expand(path_pattern || path, vars)
224
+ end
225
+
226
+ # Resolve the content-type Override part name (leading slash).
227
+ #
228
+ # @param vars [Hash] placeholder values for numbered parts
229
+ # @return [String, nil] e.g. "/word/header1.xml"
230
+ def part_name_for(**vars)
231
+ resolved = path_for(**vars)
232
+ resolved && "/#{resolved}"
233
+ end
234
+
235
+ # Resolve the relationship Target attribute.
236
+ #
237
+ # @param vars [Hash] placeholder values for numbered parts
238
+ # @return [String, nil] e.g. "header1.xml"
239
+ def target_for(**vars)
240
+ expand(target_pattern || target, vars)
241
+ end
242
+
243
+ # Fixed-path Override part name (leading slash).
244
+ #
245
+ # @return [String, nil] e.g. "/word/styles.xml"
246
+ def part_name
247
+ path && "/#{path}"
248
+ end
249
+
250
+ # Package paths this part kind emits for the given package.
251
+ #
252
+ # Single-instance kinds emit their fixed +path+ when the backing
253
+ # model is present; collection families emit each stored part's
254
+ # own paths (each part object answers +package_paths+).
255
+ #
256
+ # @param package [Docx::Package] Package to enumerate for
257
+ # @return [Array<String>] emitted package paths (possibly empty)
258
+ def emitted_paths(package)
259
+ source = attribute_source(package)
260
+ return [] if source.nil?
261
+
262
+ collection? ? collection_paths(source) : [path]
263
+ end
264
+
265
+ # Two definitions are equal when every field matches.
266
+ def ==(other)
267
+ other.is_a?(PartDefinition) && fields == other.fields
268
+ end
269
+
270
+ protected
271
+
272
+ # @return [Array] field values in declaration order, used for
273
+ # value equality
274
+ # rubocop:disable Metrics/AbcSize
275
+ def fields
276
+ [key, kind, path, path_pattern, target, target_pattern,
277
+ content_type, rel_type, extension, required, rels_scope,
278
+ standard, loader, loader_model, path_resolution,
279
+ load_priority, document_attribute, package_attribute,
280
+ copy_to_document, to_package_guard, to_package_type]
281
+ end
282
+ # rubocop:enable Metrics/AbcSize
283
+
284
+ private
285
+
286
+ # The model object backing this part kind on the package: the
287
+ # package attribute when set, else the document attribute read
288
+ # through the package's document.
289
+ def attribute_source(package)
290
+ value = package_attribute && package.method(package_attribute).call
291
+ return value if value
292
+
293
+ document_attribute &&
294
+ package.document&.method(document_attribute)&.call
295
+ end
296
+
297
+ # Families (path_pattern, no fixed path) enumerate their parts;
298
+ # single-instance kinds do not.
299
+ def collection?
300
+ path.nil?
301
+ end
302
+
303
+ def collection_paths(source)
304
+ values = source.is_a?(Docx::PartCollection) ? source.values : source
305
+ values.flat_map(&:package_paths)
306
+ end
307
+
308
+ def expand(template, vars)
309
+ return nil unless template
310
+ return template if vars.empty?
311
+
312
+ format(template, vars)
313
+ end
314
+
315
+ # Convert a pattern like "word/header%<counter>d.xml" into a
316
+ # matching regexp: %<name>d → digits, %<name>s → any path run.
317
+ def template_regex(template)
318
+ source = template.split(/(%<\w+>[ds])/).map do |part|
319
+ case part
320
+ when /%<\w+>d/ then "\\d+"
321
+ when /%<\w+>s/ then ".+?"
322
+ else Regexp.escape(part)
323
+ end
324
+ end.join
325
+ /\A#{source}\z/
326
+ end
327
+ end
328
+ end
329
+ end