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
@@ -1,8 +1,100 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uniword
4
- module Configuration
4
+ # Global runtime configuration for Uniword.
5
+ #
6
+ # Holds save-path policy as plain, explicitly typed boolean attributes.
7
+ # This object is runtime policy only: it has no file-loading behavior
8
+ # (see ConfigurationLoader for external YAML config files) and no
9
+ # serialization behavior.
10
+ #
11
+ # This class also serves as the namespace for configuration-related
12
+ # classes (ConfigurationLoader, ConfigurationError).
13
+ #
14
+ # @example Read current policy
15
+ # Uniword.configuration.validate_on_save # => true
16
+ #
17
+ # @example Change policy via Uniword.configure
18
+ # Uniword.configure do |config|
19
+ # config.xsd_validation = true
20
+ # end
21
+ class Configuration
5
22
  autoload :ConfigurationLoader,
6
23
  "#{__dir__}/configuration/configuration_loader"
24
+
25
+ # Whether documents are validated when saved.
26
+ #
27
+ # @return [Boolean]
28
+ attr_reader :validate_on_save
29
+
30
+ # Whether XSD schema validation runs on save (slower, stricter).
31
+ #
32
+ # @return [Boolean]
33
+ attr_reader :xsd_validation
34
+
35
+ # Whether automatic fixes applied during save are logged.
36
+ #
37
+ # @return [Boolean]
38
+ attr_reader :log_save_fixes
39
+
40
+ # Create a configuration with default policy values.
41
+ #
42
+ # Defaults: validate_on_save: true, xsd_validation: false,
43
+ # log_save_fixes: true.
44
+ def initialize
45
+ reset!
46
+ end
47
+
48
+ # Restore all attributes to their default values.
49
+ #
50
+ # @return [Configuration] self
51
+ def reset!
52
+ @validate_on_save = true
53
+ @xsd_validation = false
54
+ @log_save_fixes = true
55
+ self
56
+ end
57
+
58
+ # Set the validate-on-save policy.
59
+ #
60
+ # @param value [Boolean] new value
61
+ # @return [Boolean] the value set
62
+ # @raise [ArgumentError] if value is not true or false
63
+ def validate_on_save=(value)
64
+ @validate_on_save = typed_boolean(value, :validate_on_save)
65
+ end
66
+
67
+ # Set the XSD validation policy.
68
+ #
69
+ # @param value [Boolean] new value
70
+ # @return [Boolean] the value set
71
+ # @raise [ArgumentError] if value is not true or false
72
+ def xsd_validation=(value)
73
+ @xsd_validation = typed_boolean(value, :xsd_validation)
74
+ end
75
+
76
+ # Set the save-fix logging policy.
77
+ #
78
+ # @param value [Boolean] new value
79
+ # @return [Boolean] the value set
80
+ # @raise [ArgumentError] if value is not true or false
81
+ def log_save_fixes=(value)
82
+ @log_save_fixes = typed_boolean(value, :log_save_fixes)
83
+ end
84
+
85
+ private
86
+
87
+ # Validate that a value is strictly boolean.
88
+ #
89
+ # @param value [Object] value to check
90
+ # @param name [Symbol] attribute name used in the error message
91
+ # @return [Boolean] the value
92
+ # @raise [ArgumentError] if value is not true or false
93
+ def typed_boolean(value, name)
94
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
95
+
96
+ raise ArgumentError,
97
+ "#{name} must be true or false, got #{value.inspect}"
98
+ end
7
99
  end
8
100
  end
@@ -16,41 +16,20 @@ module Uniword
16
16
  #
17
17
  # Generate comprehensive [Content_Types].xml
18
18
  #
19
+ # Derived from Ooxml::PartRegistry (:standard definitions, in
20
+ # registration order — historically the literal order below).
21
+ #
19
22
  # @return [Types] Content types object
20
23
  def generate
21
24
  Types.new(
22
- defaults: [
23
- Default.new(extension: "jpeg",
24
- content_type: "image/jpeg"),
25
- Default.new(extension: "png",
26
- content_type: "image/png"),
27
- Default.new(extension: "gif",
28
- content_type: "image/gif"),
29
- Default.new(extension: "rels",
30
- content_type: "application/vnd.openxmlformats-package.relationships+xml"),
31
- Default.new(extension: "xml",
32
- content_type: "application/xml"),
33
- ],
34
- overrides: [
35
- Override.new(part_name: "/word/document.xml",
36
- content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"),
37
- Override.new(part_name: "/word/numbering.xml",
38
- content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"),
39
- Override.new(part_name: "/word/styles.xml",
40
- content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"),
41
- Override.new(part_name: "/word/settings.xml",
42
- content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"),
43
- Override.new(part_name: "/word/webSettings.xml",
44
- content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"),
45
- Override.new(part_name: "/word/fontTable.xml",
46
- content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"),
47
- Override.new(part_name: "/word/theme/theme1.xml",
48
- content_type: "application/vnd.openxmlformats-officedocument.theme+xml"),
49
- Override.new(part_name: "/docProps/core.xml",
50
- content_type: "application/vnd.openxmlformats-package.core-properties+xml"),
51
- Override.new(part_name: "/docProps/app.xml",
52
- content_type: "application/vnd.openxmlformats-officedocument.extended-properties+xml"),
53
- ],
25
+ defaults: Ooxml::PartRegistry.standard_defaults.map do |defn|
26
+ Default.new(extension: defn.extension,
27
+ content_type: defn.content_type)
28
+ end,
29
+ overrides: Ooxml::PartRegistry.standard_overrides.map do |defn|
30
+ Override.new(part_name: defn.part_name,
31
+ content_type: defn.content_type)
32
+ end,
54
33
  )
55
34
  end
56
35
  module_function :generate
@@ -59,15 +38,16 @@ module Uniword
59
38
  #
60
39
  # @return [Types] Content types object for theme package
61
40
  def generate_for_theme
41
+ theme = Ooxml::PartRegistry.find_by_key(:thmx_theme)
62
42
  Types.new(
63
- defaults: [
64
- Default.new(extension: "rels",
65
- content_type: "application/vnd.openxmlformats-package.relationships+xml"),
66
- Default.new(extension: "xml", content_type: "application/xml"),
67
- ],
43
+ defaults: %i[rels xml].map do |key|
44
+ defn = Ooxml::PartRegistry.find_by_key(key)
45
+ Default.new(extension: defn.extension,
46
+ content_type: defn.content_type)
47
+ end,
68
48
  overrides: [
69
- Override.new(part_name: "/theme/theme1.xml",
70
- content_type: "application/vnd.openxmlformats-officedocument.theme+xml"),
49
+ Override.new(part_name: theme.part_name,
50
+ content_type: theme.content_type),
71
51
  ],
72
52
  )
73
53
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # All classes autoloaded via lib/uniword.rb
4
- # No require_relative needed - autoload handles lazy loading
4
+ # Lazy loading for internal library code is handled by autoload.
5
5
 
6
6
  module Uniword
7
7
  # Factory for creating Document instances.
@@ -169,31 +169,25 @@ module Uniword
169
169
  detector.detect(path)
170
170
  end
171
171
 
172
-
173
172
  # Copy package parts to document for round-trip preservation
174
173
  #
174
+ # Registry-driven: every PartDefinition naming both a package
175
+ # and a document attribute (and not excluded via
176
+ # +copy_to_document: false+) is mirrored; a part can no longer
177
+ # exist in only one direction.
178
+ #
175
179
  # @param package [Docx::Package] The source package
176
180
  # @param document [Wordprocessingml::DocumentRoot] The target document
177
181
  # @return [void]
178
182
  def copy_package_parts_to_document(package, document)
179
183
  return unless document.is_a?(Uniword::Wordprocessingml::DocumentRoot)
180
184
 
181
- document.styles_configuration = package.styles if package.styles
182
- document.numbering_configuration = package.numbering if package.numbering
183
- document.settings = package.settings if package.settings
184
- document.font_table = package.font_table if package.font_table
185
- document.web_settings = package.web_settings if package.web_settings
186
- document.theme = package.theme if package.theme
187
- document.core_properties = package.core_properties if package.core_properties
188
- document.app_properties = package.app_properties if package.app_properties
189
- document.document_rels = package.document_rels if package.document_rels
190
- document.theme_rels = package.theme_rels if package.theme_rels
191
- document.package_rels = package.package_rels if package.package_rels
192
- document.content_types = package.content_types if package.content_types
193
- document.custom_properties = package.custom_properties if package.custom_properties
194
- document.custom_xml_items = package.custom_xml_items if package.custom_xml_items
195
- document.footnotes = package.footnotes if package.footnotes
196
- document.endnotes = package.endnotes if package.endnotes
185
+ Ooxml::PartRegistry.copied_to_document.each do |definition|
186
+ value = package.method(definition.package_attribute).call
187
+ next if value.nil?
188
+
189
+ document.method(:"#{definition.document_attribute}=").call(value)
190
+ end
197
191
  end
198
192
 
199
193
  private
@@ -33,25 +33,34 @@ module Uniword
33
33
  #
34
34
  # @param path [String] The output file path
35
35
  # @param format [Symbol] The format (:auto, :docx, :mhtml)
36
+ # @param profile [Docx::Profile, nil] Profile for reconciliation
37
+ # @param validate [Boolean, nil] Run the package integrity gate before
38
+ # writing; nil falls back to Uniword.configuration.validate_on_save.
39
+ # Not applicable to :mhtml (MIME, not an OPC/ZIP package — there are
40
+ # no content types, relationships parts, or ZIP entries to check).
36
41
  # @return [void]
37
42
  # @raise [ArgumentError] if path is invalid
38
43
  # @raise [ArgumentError] if format is not supported
44
+ # @raise [Uniword::ValidationError] when the gate is enabled and the
45
+ # generated package content is invalid
39
46
  #
40
47
  # @example Save as DOCX
41
48
  # writer.save("output.docx")
42
49
  #
43
50
  # @example Save with explicit format
44
51
  # writer.save("output.mht", format: :mhtml)
45
- def save(path, format: :auto, profile: nil)
52
+ def save(path, format: :auto, profile: nil, validate: nil)
46
53
  validate_path(path)
47
54
 
48
55
  format = infer_format(path) if format == :auto
49
56
 
50
57
  case format
51
58
  when :docx, :docm
52
- Docx::Package.to_file(document, path, profile: profile)
59
+ Docx::Package.to_file(document, path, profile: profile,
60
+ validate: validate)
53
61
  when :dotx, :dotm
54
- Ooxml::DotxPackage.to_file(document, path, profile: profile)
62
+ Ooxml::DotxPackage.to_file(document, path, profile: profile,
63
+ validate: validate)
55
64
  when :mhtml
56
65
  Mhtml::MhtmlPackage.to_file(document, path)
57
66
  else
@@ -99,6 +108,8 @@ module Uniword
99
108
  #
100
109
  # @param stream [IO, StringIO] The output stream
101
110
  # @param format [Symbol] The format (:docx, :mhtml)
111
+ # @param validate [Boolean, nil] Run the package integrity gate before
112
+ # writing; nil falls back to Uniword.configuration.validate_on_save
102
113
  # @return [void]
103
114
  #
104
115
  # @example Write to StringIO
@@ -106,14 +117,14 @@ module Uniword
106
117
  # writer.write_to_stream(io)
107
118
  # io.rewind
108
119
  # content = io.read
109
- def write_to_stream(stream, format: :docx)
120
+ def write_to_stream(stream, format: :docx, validate: nil)
110
121
  require "tempfile"
111
122
 
112
123
  # Use a temporary file to generate the document
113
124
  temp_file = Tempfile.new(["uniword_stream", ".#{format}"], binmode: true)
114
125
  begin
115
126
  # Save to temp file
116
- save(temp_file.path, format: format)
127
+ save(temp_file.path, format: format, validate: validate)
117
128
 
118
129
  # Read and write to stream in binary mode
119
130
  temp_file.rewind
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # A chart part (word/charts/chartN.xml) held by the package.
6
+ #
7
+ # The content is the raw chart XML String. Replaces the former
8
+ # +{ xml:, target: }+ hash entries in +chart_parts+.
9
+ class ChartPart < Part
10
+ # @param r_id [String, nil] relationship id
11
+ # @param target [String, nil] e.g. "charts/chart1.xml"
12
+ # @param content [String, nil] raw chart XML
13
+ def initialize(r_id: nil, target: nil, content: nil, **rest)
14
+ super(
15
+ definition: Ooxml::PartRegistry.find_by_key(:chart),
16
+ r_id: r_id, target: target, content: content, **rest
17
+ )
18
+ end
19
+
20
+ # Raw chart XML (alias for content).
21
+ #
22
+ # @return [String, nil]
23
+ def xml
24
+ content
25
+ end
26
+
27
+ # Hash-style read compatibility (+:xml+ in addition to the
28
+ # Part keys).
29
+ def [](key)
30
+ key.to_sym == :xml ? content : super
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # One customXml data item (customXml/itemN.xml) with its optional
6
+ # properties part and relationships part.
7
+ #
8
+ # Replaces the former +{ index:, xml_content:, props_xml:,
9
+ # rels_xml: }+ hash entries in +custom_xml_items+.
10
+ class CustomXmlItem
11
+ # @return [Integer] item number (customXml/item%<index>d.xml)
12
+ attr_accessor :index
13
+
14
+ # @return [String] raw XML of the item part
15
+ attr_accessor :xml_content
16
+
17
+ # @return [String, nil] raw XML of the itemProps part
18
+ attr_accessor :props_xml
19
+
20
+ # @return [String, nil] raw XML of the item relationships part
21
+ attr_accessor :rels_xml
22
+
23
+ # @param index [Integer] item number
24
+ # @param xml_content [String] raw item XML
25
+ # @param props_xml [String, nil] raw itemProps XML
26
+ # @param rels_xml [String, nil] raw item relationships XML
27
+ def initialize(index:, xml_content:, props_xml: nil, rels_xml: nil)
28
+ @index = index
29
+ @xml_content = xml_content
30
+ @props_xml = props_xml
31
+ @rels_xml = rels_xml
32
+ end
33
+
34
+ # Normalize a legacy hash entry (or an existing CustomXmlItem)
35
+ # into a CustomXmlItem.
36
+ #
37
+ # @param value [CustomXmlItem, Hash]
38
+ # @return [CustomXmlItem]
39
+ def self.wrap(value)
40
+ return value if value.is_a?(CustomXmlItem)
41
+
42
+ new(
43
+ index: value[:index],
44
+ xml_content: value[:xml_content],
45
+ props_xml: value[:props_xml],
46
+ rels_xml: value[:rels_xml],
47
+ )
48
+ end
49
+
50
+ # Package path of the item part.
51
+ #
52
+ # @return [String] e.g. "customXml/item1.xml"
53
+ def path
54
+ Ooxml::PartRegistry.find_by_key(:custom_xml_item)
55
+ .path_for(index: index)
56
+ end
57
+
58
+ # Package path of the properties part.
59
+ #
60
+ # @return [String, nil] e.g. "customXml/itemProps1.xml"
61
+ def props_path
62
+ return nil unless props_xml
63
+
64
+ Ooxml::PartRegistry.find_by_key(:custom_xml_item_props)
65
+ .path_for(index: index)
66
+ end
67
+
68
+ # Package paths this item emits (item part plus its properties
69
+ # part when present).
70
+ #
71
+ # @return [Array<String>] emitted package paths
72
+ def package_paths
73
+ [path, props_path].compact
74
+ end
75
+
76
+ # Hash-style read compatibility.
77
+ #
78
+ # @param key [Symbol, String] one of :index, :xml_content,
79
+ # :props_xml, :rels_xml
80
+ # @return [Object, nil]
81
+ def [](key)
82
+ case key.to_sym
83
+ when :index then index
84
+ when :xml_content then xml_content
85
+ when :props_xml then props_xml
86
+ when :rels_xml then rels_xml
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # A header or footer part (word/headerN.xml / word/footerN.xml)
6
+ # held by the package — the single storage form for both
7
+ # round-tripped and builder-created headers/footers.
8
+ #
9
+ # Parts loaded from an existing DOCX carry their original +r_id+,
10
+ # +target+ and +rel_type+ verbatim and are flagged +loaded+ so the
11
+ # reconciler leaves their relationships and section references
12
+ # untouched. Parts added programmatically (via the Builder or the
13
+ # +headers+/+footers+ views) are flagged fresh: the reconciler
14
+ # assigns them a relationship and wires the sectPr reference.
15
+ #
16
+ # The +type+ ("default"/"first"/"even") is the sectPr reference
17
+ # type — derived from the section properties on load, set by the
18
+ # caller on insert. Content is usually a
19
+ # Wordprocessingml::Header or Wordprocessingml::Footer model;
20
+ # legacy Uniword::Header/Uniword::Footer convenience models are
21
+ # stored as given and converted at serialization time so callers
22
+ # can keep mutating the object they inserted.
23
+ class HeaderFooterPart < Part
24
+ # Valid section reference types.
25
+ TYPES = %w[default first even].freeze
26
+
27
+ # @return [String, nil] sectPr reference type
28
+ # ("default"/"first"/"even")
29
+ attr_accessor :type
30
+
31
+ # @param kind [Symbol, nil] :header or :footer (derived from
32
+ # rel_type, target or content when omitted)
33
+ # @param type [String, nil] sectPr reference type
34
+ # @param loaded [Boolean] true when extracted verbatim from an
35
+ # existing package
36
+ def initialize(kind: nil, type: nil, loaded: false, **rest)
37
+ super(**rest)
38
+ @kind = kind
39
+ @type = type&.to_s
40
+ @loaded = loaded
41
+ end
42
+
43
+ # @return [Symbol, nil] :header or :footer
44
+ def kind
45
+ @kind ||= derive_kind
46
+ end
47
+
48
+ # @return [Boolean] true for header parts
49
+ def header?
50
+ kind == :header
51
+ end
52
+
53
+ # @return [Boolean] true for footer parts
54
+ def footer?
55
+ kind == :footer
56
+ end
57
+
58
+ # @return [Boolean] true when extracted from an existing package
59
+ # (relationships and section references are already in place)
60
+ def loaded?
61
+ !!@loaded
62
+ end
63
+
64
+ # Mark whether the part originates from a loaded package.
65
+ attr_writer :loaded
66
+
67
+ # Registry definition for this part kind.
68
+ #
69
+ # @return [Ooxml::PartDefinition, nil]
70
+ def definition
71
+ @definition ||= kind && Ooxml::PartRegistry.find_by_key(kind)
72
+ end
73
+
74
+ # -- Content delegation (quacks like the content model) --
75
+
76
+ # @return [Array<Wordprocessingml::Paragraph>] content paragraphs
77
+ def paragraphs
78
+ content&.paragraphs
79
+ end
80
+
81
+ # @return [Array<Wordprocessingml::Table>] content tables
82
+ def tables
83
+ content&.tables
84
+ end
85
+
86
+ # @return [Boolean] true when the content has no paragraphs/tables
87
+ def empty?
88
+ return true unless content
89
+
90
+ paragraphs.empty? && tables.empty?
91
+ end
92
+
93
+ # Hash-style read compatibility (+:type+ in addition to the
94
+ # Part keys).
95
+ def [](key)
96
+ key.to_sym == :type ? type : super
97
+ end
98
+
99
+ # The content model to serialize into the part file. Legacy
100
+ # Uniword::Header/Uniword::Footer models are converted to their
101
+ # Wordprocessingml counterparts (paragraph and table elements are
102
+ # shared, mc:Ignorable carried over); other content passes
103
+ # through unchanged.
104
+ #
105
+ # @return [Object] serializable content model
106
+ def serializable_content
107
+ case content
108
+ when Uniword::Header
109
+ convert_legacy(Wordprocessingml::Header.new)
110
+ when Uniword::Footer
111
+ convert_legacy(Wordprocessingml::Footer.new)
112
+ else
113
+ content
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def convert_legacy(wml)
120
+ wml.paragraphs.concat(content.paragraphs)
121
+ wml.tables.concat(content.tables)
122
+ wml.mc_ignorable = content.mc_ignorable if content.mc_ignorable
123
+ wml
124
+ end
125
+
126
+ def derive_kind
127
+ # rel_type (".../relationships/header") and target
128
+ # ("header1.xml") both embed the kind word.
129
+ carrier = "#{rel_type} #{target}"
130
+ return :header if carrier.include?("header")
131
+ return :footer if carrier.include?("footer")
132
+
133
+ case content
134
+ when Wordprocessingml::Header, Uniword::Header then :header
135
+ when Wordprocessingml::Footer, Uniword::Footer then :footer
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end