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,504 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Uniword
6
+ module Ooxml
7
+ # Single source of truth for OOXML package part metadata.
8
+ #
9
+ # Maps every part kind the library reads or writes to its package
10
+ # path, content type, relationship type, [Content_Types].xml entry
11
+ # form (Default vs Override), loader strategy, and document/package
12
+ # attributes. Content type and relationship type literals live ONLY
13
+ # here; consumers (Uniword::ContentTypes, Docx::PackageDefaults,
14
+ # Docx::Reconciler::PackageStructure, Docx::PartLoader,
15
+ # DocumentFactory, and the Docx::PackageSerialization inject_*
16
+ # methods) derive from this registry instead of holding their own
17
+ # literals.
18
+ #
19
+ # Open/closed: a new part kind is added by registering a
20
+ # PartDefinition — consumers need no further changes.
21
+ #
22
+ # @example Look up a part by key
23
+ # defn = PartRegistry.find_by_key(:styles)
24
+ # defn.part_name # => "/word/styles.xml"
25
+ # defn.rel_type # => ".../relationships/styles"
26
+ #
27
+ # @example Register a custom part kind
28
+ # PartRegistry.register(
29
+ # PartDefinition.new(key: :glossary, path: "word/glossary/document.xml",
30
+ # target: "glossary/document.xml",
31
+ # content_type: "...document.glossary+xml",
32
+ # rel_type: ".../glossaryDocument",
33
+ # kind: :override, rels_scope: :document)
34
+ # )
35
+ module PartRegistry
36
+ # Relationship type base URIs. These literals appear only here.
37
+ OFFICE_REL_BASE =
38
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
39
+ PACKAGE_REL_BASE =
40
+ "http://schemas.openxmlformats.org/package/2006/relationships"
41
+
42
+ # Content type prefixes (literals appear only here).
43
+ CT_OFFICE = "application/vnd.openxmlformats-officedocument"
44
+ CT_WML = "#{CT_OFFICE}.wordprocessingml".freeze
45
+ CT_PACKAGE = "application/vnd.openxmlformats-package"
46
+
47
+ # Built-in registrations. Registration order is observable in
48
+ # emitted output: the :standard entries reproduce the historic
49
+ # ContentTypes.generate ordering exactly (defaults first —
50
+ # jpeg, png, gif, rels, xml — then overrides in the historic
51
+ # order). Non-standard parts follow; consumers select their own
52
+ # ordered subsets by key.
53
+ #
54
+ # Loader metadata: +loader+ selects the Docx::PartLoader
55
+ # strategy, +loader_model+ the parsing model class,
56
+ # +load_priority+ orders the load (content types and package
57
+ # rels first; the main document before the parts its rels
58
+ # describe), and +document_attribute+/+package_attribute+ name
59
+ # where the part lives on Wordprocessingml::DocumentRoot and
60
+ # Docx::Package for the two registry-driven copy loops.
61
+ BUILT_INS = [
62
+ # -- Content-type Defaults (extension → content type) --
63
+ { key: :jpeg, kind: :default, extension: "jpeg",
64
+ content_type: "image/jpeg", standard: true },
65
+ { key: :png, kind: :default, extension: "png",
66
+ content_type: "image/png", standard: true },
67
+ { key: :gif, kind: :default, extension: "gif",
68
+ content_type: "image/gif", standard: true },
69
+ { key: :rels, kind: :default, extension: "rels",
70
+ content_type: "#{CT_PACKAGE}.relationships+xml",
71
+ required: true, standard: true },
72
+ { key: :xml, kind: :default, extension: "xml",
73
+ content_type: "application/xml",
74
+ required: true, standard: true },
75
+
76
+ # -- Standard Override parts (historic generate order) --
77
+ { key: :document, kind: :override, path: "word/document.xml",
78
+ content_type: "#{CT_WML}.document.main+xml",
79
+ rel_type: "#{OFFICE_REL_BASE}/officeDocument",
80
+ required: true, standard: true, rels_scope: :package,
81
+ loader: :xml_model,
82
+ loader_model: Uniword::Wordprocessingml::DocumentRoot,
83
+ path_resolution: :office_document, load_priority: 40,
84
+ package_attribute: :document },
85
+ { key: :numbering, kind: :override, path: "word/numbering.xml",
86
+ target: "numbering.xml", content_type: "#{CT_WML}.numbering+xml",
87
+ rel_type: "#{OFFICE_REL_BASE}/numbering",
88
+ standard: true, rels_scope: :document,
89
+ loader: :xml_model,
90
+ loader_model: Uniword::Wordprocessingml::NumberingConfiguration,
91
+ load_priority: 50,
92
+ package_attribute: :numbering,
93
+ document_attribute: :numbering_configuration,
94
+ to_package_guard: :numbering_configuration_loaded? },
95
+ { key: :styles, kind: :override, path: "word/styles.xml",
96
+ target: "styles.xml", content_type: "#{CT_WML}.styles+xml",
97
+ rel_type: "#{OFFICE_REL_BASE}/styles",
98
+ required: true, standard: true, rels_scope: :document,
99
+ loader: :xml_model,
100
+ loader_model: Uniword::Wordprocessingml::StylesConfiguration,
101
+ load_priority: 50,
102
+ package_attribute: :styles,
103
+ document_attribute: :styles_configuration },
104
+ { key: :settings, kind: :override, path: "word/settings.xml",
105
+ target: "settings.xml", content_type: "#{CT_WML}.settings+xml",
106
+ rel_type: "#{OFFICE_REL_BASE}/settings",
107
+ required: true, standard: true, rels_scope: :document,
108
+ loader: :xml_model,
109
+ loader_model: Uniword::Wordprocessingml::Settings,
110
+ load_priority: 50,
111
+ package_attribute: :settings, document_attribute: :settings },
112
+ { key: :web_settings, kind: :override, path: "word/webSettings.xml",
113
+ target: "webSettings.xml",
114
+ content_type: "#{CT_WML}.webSettings+xml",
115
+ rel_type: "#{OFFICE_REL_BASE}/webSettings",
116
+ required: true, standard: true, rels_scope: :document,
117
+ loader: :xml_model,
118
+ loader_model: Uniword::Wordprocessingml::WebSettings,
119
+ load_priority: 50,
120
+ package_attribute: :web_settings,
121
+ document_attribute: :web_settings },
122
+ { key: :font_table, kind: :override, path: "word/fontTable.xml",
123
+ target: "fontTable.xml",
124
+ content_type: "#{CT_WML}.fontTable+xml",
125
+ rel_type: "#{OFFICE_REL_BASE}/fontTable",
126
+ required: true, standard: true, rels_scope: :document,
127
+ loader: :xml_model,
128
+ loader_model: Uniword::Wordprocessingml::FontTable,
129
+ load_priority: 50,
130
+ package_attribute: :font_table,
131
+ document_attribute: :font_table },
132
+ { key: :theme, kind: :override, path: "word/theme/theme1.xml",
133
+ target: "theme/theme1.xml",
134
+ content_type: "#{CT_OFFICE}.theme+xml",
135
+ rel_type: "#{OFFICE_REL_BASE}/theme",
136
+ standard: true, rels_scope: :document,
137
+ loader: :xml_model, loader_model: Uniword::Drawingml::Theme,
138
+ load_priority: 70,
139
+ package_attribute: :theme, document_attribute: :theme },
140
+ { key: :core_properties, kind: :override, path: "docProps/core.xml",
141
+ content_type: "#{CT_PACKAGE}.core-properties+xml",
142
+ rel_type: "#{PACKAGE_REL_BASE}/metadata/core-properties",
143
+ required: true, standard: true, rels_scope: :package,
144
+ loader: :xml_model, loader_model: CoreProperties,
145
+ load_priority: 30,
146
+ package_attribute: :core_properties,
147
+ document_attribute: :core_properties },
148
+ { key: :app_properties, kind: :override, path: "docProps/app.xml",
149
+ content_type: "#{CT_OFFICE}.extended-properties+xml",
150
+ rel_type: "#{OFFICE_REL_BASE}/extended-properties",
151
+ required: true, standard: true, rels_scope: :package,
152
+ loader: :xml_model, loader_model: AppProperties,
153
+ load_priority: 30,
154
+ package_attribute: :app_properties,
155
+ document_attribute: :app_properties },
156
+
157
+ # -- Optional document parts --
158
+ { key: :footnotes, kind: :override, path: "word/footnotes.xml",
159
+ target: "footnotes.xml",
160
+ content_type: "#{CT_WML}.footnotes+xml",
161
+ rel_type: "#{OFFICE_REL_BASE}/footnotes",
162
+ rels_scope: :document,
163
+ loader: :xml_model,
164
+ loader_model: Uniword::Wordprocessingml::Footnotes,
165
+ load_priority: 80,
166
+ package_attribute: :footnotes, document_attribute: :footnotes },
167
+ { key: :endnotes, kind: :override, path: "word/endnotes.xml",
168
+ target: "endnotes.xml",
169
+ content_type: "#{CT_WML}.endnotes+xml",
170
+ rel_type: "#{OFFICE_REL_BASE}/endnotes",
171
+ rels_scope: :document,
172
+ loader: :xml_model,
173
+ loader_model: Uniword::Wordprocessingml::Endnotes,
174
+ load_priority: 80,
175
+ package_attribute: :endnotes, document_attribute: :endnotes },
176
+ { key: :comments, kind: :override, path: "word/comments.xml",
177
+ target: "comments.xml",
178
+ content_type: "#{CT_WML}.comments+xml",
179
+ rel_type: "#{OFFICE_REL_BASE}/comments",
180
+ rels_scope: :document,
181
+ loader: :xml_model, loader_model: Uniword::CommentsPart,
182
+ load_priority: 80,
183
+ package_attribute: :comments, document_attribute: :comments,
184
+ to_package_type: Uniword::CommentsPart },
185
+ # Bibliography sources live only on the document (single home:
186
+ # DocumentRoot#bibliography_sources); nothing reads
187
+ # word/sources.xml back, so there is no loader.
188
+ { key: :bibliography, kind: :override, path: "word/sources.xml",
189
+ target: "sources.xml",
190
+ content_type: "#{CT_OFFICE}.bibliography+xml",
191
+ rel_type: "#{OFFICE_REL_BASE}/bibliography",
192
+ rels_scope: :document,
193
+ document_attribute: :bibliography_sources },
194
+ { key: :header, kind: :override,
195
+ path_pattern: "word/header%<counter>d.xml",
196
+ target_pattern: "header%<counter>d.xml",
197
+ content_type: "#{CT_WML}.header+xml",
198
+ rel_type: "#{OFFICE_REL_BASE}/header",
199
+ rels_scope: :document,
200
+ loader: :header_footer,
201
+ loader_model: Uniword::Wordprocessingml::Header,
202
+ load_priority: 90,
203
+ document_attribute: :header_footer_parts },
204
+ { key: :footer, kind: :override,
205
+ path_pattern: "word/footer%<counter>d.xml",
206
+ target_pattern: "footer%<counter>d.xml",
207
+ content_type: "#{CT_WML}.footer+xml",
208
+ rel_type: "#{OFFICE_REL_BASE}/footer",
209
+ rels_scope: :document,
210
+ loader: :header_footer,
211
+ loader_model: Uniword::Wordprocessingml::Footer,
212
+ load_priority: 90,
213
+ document_attribute: :header_footer_parts },
214
+ { key: :custom_properties, kind: :override,
215
+ path: "docProps/custom.xml",
216
+ content_type: "#{CT_OFFICE}.custom-properties+xml",
217
+ rel_type: "#{OFFICE_REL_BASE}/custom-properties",
218
+ rels_scope: :package,
219
+ loader: :xml_model, loader_model: CustomProperties,
220
+ load_priority: 30,
221
+ package_attribute: :custom_properties,
222
+ document_attribute: :custom_properties },
223
+ # The chart loader keys parts by document relationship id, so
224
+ # they land directly on the document — the single home for
225
+ # chart parts (DocumentRoot#chart_parts).
226
+ { key: :chart, kind: :override,
227
+ path_pattern: "word/charts/chart%<n>d.xml",
228
+ target_pattern: "charts/chart%<n>d.xml",
229
+ content_type: "#{CT_OFFICE}.drawingml.chart+xml",
230
+ rel_type: "#{OFFICE_REL_BASE}/chart",
231
+ rels_scope: :document,
232
+ loader: :chart, load_priority: 100,
233
+ document_attribute: :chart_parts },
234
+ # Images carry a per-extension Default content type resolved
235
+ # from the image data at save time, so content_type is nil.
236
+ { key: :image, kind: :default,
237
+ path_pattern: "word/media/%<name>s",
238
+ target_pattern: "media/%<name>s",
239
+ rel_type: "#{OFFICE_REL_BASE}/image",
240
+ rels_scope: :document,
241
+ loader: :image, load_priority: 110,
242
+ document_attribute: :image_parts },
243
+ # The embedding loader keys parts by target on the package's
244
+ # embeddings collection; no package→document copy.
245
+ { key: :ole_object, kind: :override,
246
+ path_pattern: "word/embeddings/%<name>s",
247
+ target_pattern: "embeddings/%<name>s",
248
+ content_type: "#{CT_OFFICE}.oleObject",
249
+ rel_type: "#{OFFICE_REL_BASE}/oleObject",
250
+ rels_scope: :document,
251
+ loader: :embedding, load_priority: 120,
252
+ package_attribute: :embeddings,
253
+ document_attribute: :embeddings,
254
+ copy_to_document: false },
255
+ # customXml items themselves fall under the "xml" Default;
256
+ # only their itemProps parts get an Override. The :custom_xml
257
+ # loader also pulls each item's itemProps and .rels sidecars.
258
+ { key: :custom_xml_item, kind: :none,
259
+ path_pattern: "customXml/item%<index>d.xml",
260
+ loader: :custom_xml, load_priority: 30,
261
+ package_attribute: :custom_xml_items,
262
+ document_attribute: :custom_xml_items },
263
+ { key: :custom_xml_item_props, kind: :override,
264
+ path_pattern: "customXml/itemProps%<index>d.xml",
265
+ content_type: "#{CT_OFFICE}.customXmlProperties+xml" },
266
+ # Hyperlink relationships target external URLs; no part, no
267
+ # content type.
268
+ { key: :hyperlink, kind: :none,
269
+ rel_type: "#{OFFICE_REL_BASE}/hyperlink",
270
+ rels_scope: :document },
271
+ # Theme part of THMX (theme) packages, rooted at /theme.
272
+ { key: :thmx_theme, kind: :override, path: "theme/theme1.xml",
273
+ content_type: "#{CT_OFFICE}.theme+xml", required: true },
274
+
275
+ # -- Read-side infrastructure parts (no content-type entry
276
+ # of their own; kind :none keeps emission untouched) --
277
+ { key: :content_types, kind: :none, path: "[Content_Types].xml",
278
+ loader: :xml_model, loader_model: Uniword::ContentTypes::Types,
279
+ load_priority: 10,
280
+ package_attribute: :content_types,
281
+ document_attribute: :content_types },
282
+ { key: :package_rels, kind: :none, path: "_rels/.rels",
283
+ loader: :xml_model,
284
+ loader_model: Relationships::PackageRelationships,
285
+ load_priority: 20,
286
+ package_attribute: :package_rels,
287
+ document_attribute: :package_rels },
288
+ { key: :document_rels, kind: :none,
289
+ path: "word/_rels/document.xml.rels",
290
+ loader: :xml_model,
291
+ loader_model: Relationships::PackageRelationships,
292
+ path_resolution: :office_document_rels, load_priority: 60,
293
+ package_attribute: :document_rels,
294
+ document_attribute: :document_rels },
295
+ { key: :settings_rels, kind: :none,
296
+ path: "word/_rels/settings.xml.rels",
297
+ loader: :xml_model,
298
+ loader_model: Relationships::PackageRelationships,
299
+ load_priority: 50,
300
+ package_attribute: :settings_rels,
301
+ document_attribute: :settings_rels },
302
+ { key: :theme_rels, kind: :none,
303
+ path: "word/theme/_rels/theme1.xml.rels",
304
+ loader: :xml_model,
305
+ loader_model: Relationships::PackageRelationships,
306
+ load_priority: 75,
307
+ package_attribute: :theme_rels,
308
+ document_attribute: :theme_rels },
309
+ { key: :footnotes_rels, kind: :none,
310
+ path: "word/_rels/footnotes.xml.rels",
311
+ loader: :xml_model,
312
+ loader_model: Relationships::PackageRelationships,
313
+ load_priority: 85,
314
+ package_attribute: :footnotes_rels,
315
+ document_attribute: :footnotes_rels },
316
+ { key: :endnotes_rels, kind: :none,
317
+ path: "word/_rels/endnotes.xml.rels",
318
+ loader: :xml_model,
319
+ loader_model: Relationships::PackageRelationships,
320
+ load_priority: 85,
321
+ package_attribute: :endnotes_rels,
322
+ document_attribute: :endnotes_rels },
323
+ # Theme media files live on the theme's media_files, not on a
324
+ # package/document attribute; loads only when a theme part was
325
+ # parsed (see PartLoader::ThemeMediaLoader).
326
+ { key: :theme_media, kind: :none,
327
+ path_pattern: "word/theme/media/%<name>s",
328
+ loader: :theme_media, load_priority: 72 },
329
+ # Fallback passthrough for parts no definition claims
330
+ # (unmodelled parts such as docProps/meta.xml, glossary
331
+ # documents, VBA projects): the :raw loader runs last and
332
+ # carries the remainder byte-for-byte on the package's
333
+ # raw_parts collection. Emission is derived from the stored
334
+ # parts like any collection family, so emitted_paths — and
335
+ # with it the reconciler's dangling-relationship sweep — sees
336
+ # raw-carried parts as first-class.
337
+ { key: :raw_parts, kind: :none,
338
+ loader: :raw, load_priority: 130,
339
+ package_attribute: :raw_parts,
340
+ document_attribute: :raw_parts },
341
+ ].freeze
342
+
343
+ class << self
344
+ # Register a part definition. Re-registering an existing key
345
+ # replaces it in place (preserving position); new keys append.
346
+ #
347
+ # @param definition [PartDefinition] definition to register
348
+ # @return [PartDefinition] the registered definition
349
+ # @raise [ArgumentError] when not given a PartDefinition
350
+ def register(definition)
351
+ unless definition.is_a?(PartDefinition)
352
+ raise ArgumentError,
353
+ "expected PartDefinition, got #{definition.class}"
354
+ end
355
+
356
+ index = definitions.index { |d| d.key == definition.key }
357
+ if index
358
+ definitions[index] = definition
359
+ else
360
+ definitions << definition
361
+ end
362
+ definition
363
+ end
364
+
365
+ # Remove a registered definition (primarily for tests).
366
+ #
367
+ # @param key [Symbol] registry key
368
+ # @return [Array<PartDefinition>, nil] the definitions list if
369
+ # a definition was removed, nil when the key was absent
370
+ def unregister(key)
371
+ definitions.reject! { |d| d.key == key.to_sym }
372
+ end
373
+
374
+ # @return [Array<PartDefinition>] all definitions, in
375
+ # registration order (a copy; safe to mutate)
376
+ def all
377
+ definitions.dup
378
+ end
379
+
380
+ # @param key [Symbol] registry key (e.g. :styles)
381
+ # @return [PartDefinition, nil]
382
+ def find_by_key(key)
383
+ definitions.find { |d| d.key == key.to_sym }
384
+ end
385
+
386
+ # Find by package path; accepts both "word/styles.xml" and
387
+ # "/word/styles.xml" forms, and matches numbered paths against
388
+ # registered patterns ("word/header2.xml" → :header).
389
+ #
390
+ # @param path [String] package-relative path
391
+ # @return [PartDefinition, nil]
392
+ def find_by_path(path)
393
+ normalized = path.to_s.delete_prefix("/")
394
+ definitions.find { |d| path_match?(d, normalized) }
395
+ end
396
+
397
+ # @param content_type [String] MIME content type
398
+ # @return [PartDefinition, nil] first definition with this type
399
+ def find_by_content_type(content_type)
400
+ definitions.find { |d| d.content_type == content_type }
401
+ end
402
+
403
+ # Find an Override definition by content-type part name.
404
+ #
405
+ # @param part_name [String] e.g. "/word/styles.xml"
406
+ # @return [PartDefinition, nil]
407
+ def override_for(part_name)
408
+ normalized = part_name.to_s.delete_prefix("/")
409
+ definitions.find do |d|
410
+ d.override? && path_match?(d, normalized)
411
+ end
412
+ end
413
+
414
+ # Find a Default definition by file extension.
415
+ #
416
+ # @param extension [String] e.g. "png"
417
+ # @return [PartDefinition, nil]
418
+ def default_for(extension)
419
+ definitions.find do |d|
420
+ d.default? && d.extension == extension.to_s
421
+ end
422
+ end
423
+
424
+ # Definitions included in the comprehensive [Content_Types].xml
425
+ # that ContentTypes.generate emits for new DOCX packages.
426
+ #
427
+ # @return [Array<PartDefinition>] in registration order
428
+ def standard_defaults
429
+ definitions.select { |d| d.standard? && d.default? }
430
+ end
431
+
432
+ # @return [Array<PartDefinition>] in registration order
433
+ def standard_overrides
434
+ definitions.select { |d| d.standard? && d.override? }
435
+ end
436
+
437
+ # Relationship type URIs whose relationships belong in the
438
+ # package-level _rels/.rels part.
439
+ #
440
+ # @return [Array<String>] in registration order
441
+ def package_rel_types
442
+ definitions.filter_map { |d| d.rel_type if d.package_rel? }
443
+ end
444
+
445
+ # Definitions loadable from extracted ZIP content, in load
446
+ # order: ascending PartDefinition#load_priority, registration
447
+ # order within a priority (explicit index tiebreak — Ruby's
448
+ # sort_by is not stable).
449
+ #
450
+ # @return [Array<PartDefinition>]
451
+ def loadable
452
+ definitions.select(&:loadable?).each_with_index
453
+ .sort_by { |d, i| [d.load_priority || 100, i] }
454
+ .map(&:first)
455
+ end
456
+
457
+ # Definitions mirrored package→document by
458
+ # DocumentFactory.copy_package_parts_to_document.
459
+ #
460
+ # @return [Array<PartDefinition>] in registration order
461
+ def copied_to_document
462
+ definitions.select do |d|
463
+ d.document_attribute && d.package_attribute &&
464
+ d.copy_to_document?
465
+ end
466
+ end
467
+
468
+ # Definitions mirrored document→package by
469
+ # Docx::PackageDefaults.copy_document_parts_to_package.
470
+ #
471
+ # @return [Array<PartDefinition>] in registration order
472
+ def copied_to_package
473
+ definitions.select { |d| d.document_attribute && d.package_attribute }
474
+ end
475
+
476
+ # Package paths the given package will emit, derived from the
477
+ # definitions: single-instance kinds by model presence,
478
+ # collection families by their stored parts.
479
+ #
480
+ # Single authority for "will this part be emitted?" — used by
481
+ # the reconciler's dangling-relationship sweep.
482
+ #
483
+ # @param package [Docx::Package] Package to enumerate for
484
+ # @return [Set<String>] emitted package paths
485
+ def emitted_paths(package)
486
+ all.flat_map { |definition| definition.emitted_paths(package) }
487
+ .compact.to_set
488
+ end
489
+
490
+ private
491
+
492
+ def definitions
493
+ @definitions ||= []
494
+ end
495
+
496
+ def path_match?(definition, normalized_path)
497
+ definition.match_path?(normalized_path)
498
+ end
499
+ end
500
+
501
+ BUILT_INS.each { |attrs| register(PartDefinition.new(**attrs)) }
502
+ end
503
+ end
504
+ end
@@ -9,10 +9,13 @@ module Uniword
9
9
  #
10
10
  # Used for image relationships in documents
11
11
  class ImageRelationship < Relationship
12
- def initialize(target:)
12
+ # @param id [String] relationship ID (allocated by
13
+ # Docx::IdAllocator — the single rId authority)
14
+ # @param target [String] image part target (e.g. "media/image1.png")
15
+ def initialize(id:, target:)
13
16
  super(
14
- id: "rId#{SecureRandom.hex(4)}",
15
- type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
17
+ id: id,
18
+ type: PartRegistry.find_by_key(:image).rel_type,
16
19
  target: target
17
20
  )
18
21
  end
@@ -23,35 +23,33 @@ module Uniword
23
23
  #
24
24
  # Generate package-level .rels file
25
25
  #
26
+ # Historical rId assignment: rId3 = app, rId2 = core,
27
+ # rId1 = document (kept for byte-identical DOTX output).
28
+ #
26
29
  # @return [PackageRelationships] Relationships object for _rels/.rels
27
- def self.next_available_rid(relationships)
28
- max = relationships.relationships.filter_map do |r|
29
- r.id[/\ArId(\d+)\z/, 1]&.to_i
30
- end.max || 0
31
- "rId#{max + 1}"
32
- end
33
-
34
30
  def self.generate_package_rels
35
31
  new(
36
32
  relationships: [
37
- PackageRelationship.new(
38
- id: "rId3",
39
- type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
40
- target: "docProps/app.xml",
41
- ),
42
- PackageRelationship.new(
43
- id: "rId2",
44
- type: "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
45
- target: "docProps/core.xml",
46
- ),
47
- PackageRelationship.new(
48
- id: "rId1",
49
- type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
50
- target: "word/document.xml",
51
- ),
33
+ package_relationship("rId3", :app_properties),
34
+ package_relationship("rId2", :core_properties),
35
+ package_relationship("rId1", :document),
52
36
  ],
53
37
  )
54
38
  end
39
+
40
+ # Build one package-level relationship from the registry.
41
+ #
42
+ # @param rid [String] relationship ID
43
+ # @param key [Symbol] PartRegistry key
44
+ # @return [PackageRelationship]
45
+ def self.package_relationship(rid, key)
46
+ definition = PartRegistry.find_by_key(key)
47
+ PackageRelationship.new(
48
+ id: rid,
49
+ type: definition.rel_type,
50
+ target: definition.target,
51
+ )
52
+ end
55
53
  end
56
54
  end
57
55
  end
@@ -301,12 +301,12 @@ module Uniword
301
301
  def get_attribute_value(element, attr_def)
302
302
  property_name = attr_def.property_name
303
303
 
304
- # public_send is the canonical Ruby idiom for schema-driven
305
- # serialization: reading attribute values by name from any
306
- # Lutaml::Model::Serializable subclass. The property_name comes
307
- # from the schema definition, not user input.
304
+ # Schema-driven read: the model's declared attributes are the
305
+ # allowlist, and the value is fetched through the model's
306
+ # public method table. The property_name comes from the schema
307
+ # definition, not user input.
308
308
  if element.is_a?(Lutaml::Model::Serializable) && element.class.attributes.key?(property_name)
309
- element.public_send(property_name)
309
+ element.method(property_name).call
310
310
  elsif element.is_a?(Hash) && element.key?(property_name)
311
311
  element[property_name]
312
312
  end
@@ -320,9 +320,10 @@ module Uniword
320
320
  def get_child_elements(element, child_def)
321
321
  property_name = child_def.property_name
322
322
 
323
- # Try to get children using property name
323
+ # Schema-driven read via the model's public method table,
324
+ # guarded by the declared attribute names.
324
325
  if element.is_a?(Lutaml::Model::Serializable) && element.class.attributes.key?(property_name)
325
- element.public_send(property_name)
326
+ element.method(property_name).call
326
327
  elsif element.is_a?(Hash) && element.key?(property_name)
327
328
  element[property_name]
328
329
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Uniword
6
+ module Ooxml
7
+ module Types
8
+ # ST_HexColor (ECMA-376): RGB hex color or "auto"
9
+ #
10
+ # Constrained string type: six hexadecimal digits (case-insensitive,
11
+ # ST_HexColorRGB) or the literal "auto" (ST_HexColorAuto). Invalid
12
+ # values raise Lutaml::Model::Type::InvalidValueError at cast time
13
+ # (attribute assignment and XML parsing).
14
+ class HexColorValue < Lutaml::Model::Type::String
15
+ # ST_HexColor value shape: "auto" or RRGGBB hex digits
16
+ HEX_COLOR_PATTERN = /\A(?:auto|[0-9A-Fa-f]{6})\z/
17
+
18
+ # Cast a value to a valid ST_HexColor string
19
+ #
20
+ # @param value [Object] the raw value
21
+ # @param options [Hash] cast options (unused)
22
+ # @return [String, nil] the validated color string
23
+ # @raise [Lutaml::Model::Type::InvalidValueError] when the value
24
+ # is neither "auto" nor six hex digits
25
+ def self.cast(value, options = {})
26
+ casted = super
27
+ return casted if casted.nil? ||
28
+ Lutaml::Model::Utils.uninitialized?(casted)
29
+ return casted if HEX_COLOR_PATTERN.match?(casted)
30
+
31
+ raise Lutaml::Model::Type::InvalidValueError.new(
32
+ value, ["auto", "RRGGBB (6 hexadecimal digits)"]
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end