uniword 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +223 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/config/ooxml/schemas/shared_types.yml +5 -4
  5. data/data/schemas/ecma/dc.xsd +118 -0
  6. data/data/schemas/ecma/dcmitype.xsd +50 -0
  7. data/data/schemas/ecma/dcterms.xsd +331 -0
  8. data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
  9. data/data/schemas/ecma/xml.xsd +117 -0
  10. data/lib/uniword/builder/bibliography_builder.rb +1 -1
  11. data/lib/uniword/builder/chart_builder.rb +19 -9
  12. data/lib/uniword/builder/comment_anchorer.rb +221 -0
  13. data/lib/uniword/builder/document_builder.rb +97 -6
  14. data/lib/uniword/builder/image_builder.rb +6 -5
  15. data/lib/uniword/builder/run_utils.rb +2 -0
  16. data/lib/uniword/builder.rb +1 -0
  17. data/lib/uniword/cli/fonts_cli.rb +52 -0
  18. data/lib/uniword/cli/main.rb +110 -11
  19. data/lib/uniword/cli/page_cli.rb +85 -0
  20. data/lib/uniword/cli/styles_cli.rb +204 -0
  21. data/lib/uniword/cli/theme_cli.rb +88 -0
  22. data/lib/uniword/cli/toc_cli.rb +22 -2
  23. data/lib/uniword/comment.rb +20 -15
  24. data/lib/uniword/comment_range.rb +3 -3
  25. data/lib/uniword/comments_part.rb +51 -19
  26. data/lib/uniword/configuration/configuration_loader.rb +5 -3
  27. data/lib/uniword/configuration.rb +93 -1
  28. data/lib/uniword/content_types.rb +19 -39
  29. data/lib/uniword/document_factory.rb +9 -1
  30. data/lib/uniword/document_writer.rb +16 -5
  31. data/lib/uniword/docx/chart_part.rb +34 -0
  32. data/lib/uniword/docx/custom_xml_item.rb +83 -0
  33. data/lib/uniword/docx/header_footer_part.rb +140 -0
  34. data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
  35. data/lib/uniword/docx/header_footer_view.rb +225 -0
  36. data/lib/uniword/docx/id_allocator.rb +130 -36
  37. data/lib/uniword/docx/package.rb +223 -35
  38. data/lib/uniword/docx/package_defaults.rb +58 -78
  39. data/lib/uniword/docx/package_integrity_checker.rb +312 -0
  40. data/lib/uniword/docx/package_serialization.rb +103 -231
  41. data/lib/uniword/docx/part.rb +106 -0
  42. data/lib/uniword/docx/part_collection.rb +123 -0
  43. data/lib/uniword/docx/reconciler/body.rb +59 -68
  44. data/lib/uniword/docx/reconciler/fix.rb +46 -0
  45. data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
  46. data/lib/uniword/docx/reconciler/helpers.rb +9 -1
  47. data/lib/uniword/docx/reconciler/notes.rb +17 -8
  48. data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
  49. data/lib/uniword/docx/reconciler/parts.rb +36 -13
  50. data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
  51. data/lib/uniword/docx/reconciler/tables.rb +78 -16
  52. data/lib/uniword/docx/reconciler/theme.rb +7 -4
  53. data/lib/uniword/docx/reconciler.rb +36 -14
  54. data/lib/uniword/docx.rb +10 -0
  55. data/lib/uniword/drawingml/blip.rb +0 -1
  56. data/lib/uniword/drawingml/color_scheme.rb +5 -6
  57. data/lib/uniword/drawingml/font_scheme.rb +4 -8
  58. data/lib/uniword/drawingml/theme.rb +12 -6
  59. data/lib/uniword/errors.rb +8 -1
  60. data/lib/uniword/footer.rb +4 -0
  61. data/lib/uniword/header.rb +4 -0
  62. data/lib/uniword/hyperlink.rb +1 -1
  63. data/lib/uniword/image.rb +6 -6
  64. data/lib/uniword/lazy_loader.rb +1 -2
  65. data/lib/uniword/model_attribute_access.rb +20 -4
  66. data/lib/uniword/ooxml/dotx_package.rb +42 -2
  67. data/lib/uniword/ooxml/part_definition.rb +171 -0
  68. data/lib/uniword/ooxml/part_registry.rb +311 -0
  69. data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
  70. data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
  71. data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
  72. data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
  73. data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
  74. data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
  75. data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
  76. data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
  77. data/lib/uniword/ooxml/types.rb +10 -0
  78. data/lib/uniword/ooxml.rb +2 -0
  79. data/lib/uniword/properties/alignment.rb +8 -3
  80. data/lib/uniword/properties/border.rb +52 -6
  81. data/lib/uniword/properties/cell_vertical_align.rb +4 -1
  82. data/lib/uniword/properties/color_value.rb +8 -3
  83. data/lib/uniword/properties/highlight.rb +9 -5
  84. data/lib/uniword/properties/shading.rb +4 -3
  85. data/lib/uniword/properties/tab_stop.rb +6 -1
  86. data/lib/uniword/properties/table_justification.rb +4 -1
  87. data/lib/uniword/properties/underline.rb +4 -2
  88. data/lib/uniword/properties/vertical_align.rb +6 -3
  89. data/lib/uniword/review/review_manager.rb +23 -9
  90. data/lib/uniword/revision.rb +9 -9
  91. data/lib/uniword/schema/model_generator.rb +31 -15
  92. data/lib/uniword/shared_types/hex_color.rb +4 -1
  93. data/lib/uniword/shared_types/pixel_measure.rb +4 -1
  94. data/lib/uniword/shared_types/point_measure.rb +4 -1
  95. data/lib/uniword/shared_types/text_alignment.rb +6 -1
  96. data/lib/uniword/shared_types/twips_measure.rb +4 -1
  97. data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
  98. data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
  99. data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
  100. data/lib/uniword/template/variable_resolver.rb +40 -7
  101. data/lib/uniword/themes/theme_transformation.rb +55 -41
  102. data/lib/uniword/toc/toc_generator.rb +2 -0
  103. data/lib/uniword/validation/engine.rb +35 -0
  104. data/lib/uniword/validation/link_checker.rb +1 -1
  105. data/lib/uniword/validation/opc_validator.rb +0 -1
  106. data/lib/uniword/validation/report/layer_result.rb +0 -1
  107. data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
  108. data/lib/uniword/validation/report/verification_report.rb +0 -2
  109. data/lib/uniword/validation/report.rb +20 -0
  110. data/lib/uniword/validation/rules/base.rb +10 -2
  111. data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
  112. data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
  113. data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
  114. data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
  115. data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
  116. data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
  117. data/lib/uniword/validation/rules/document_context.rb +7 -0
  118. data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
  119. data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
  120. data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
  121. data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
  122. data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
  123. data/lib/uniword/validation/rules/images_rule.rb +0 -2
  124. data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
  125. data/lib/uniword/validation/rules/model_context.rb +32 -0
  126. data/lib/uniword/validation/rules/model_rule.rb +34 -0
  127. data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
  128. data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
  129. data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
  130. data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
  131. data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
  132. data/lib/uniword/validation/rules/settings_rule.rb +0 -2
  133. data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
  134. data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
  135. data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
  136. data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
  137. data/lib/uniword/validation/rules/tables_rule.rb +0 -2
  138. data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
  139. data/lib/uniword/validation/rules/theme_rule.rb +0 -2
  140. data/lib/uniword/validation/rules.rb +61 -23
  141. data/lib/uniword/validation/schema_registry.rb +11 -0
  142. data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
  143. data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
  144. data/lib/uniword/validation/validators.rb +0 -9
  145. data/lib/uniword/validation/verify_orchestrator.rb +0 -7
  146. data/lib/uniword/validation.rb +7 -2
  147. data/lib/uniword/version.rb +1 -1
  148. data/lib/uniword/vml/imagedata.rb +0 -1
  149. data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
  150. data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
  151. data/lib/uniword/wordprocessingml/document_root.rb +211 -21
  152. data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
  153. data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
  154. data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
  155. data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
  156. data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
  157. data/lib/uniword/wordprocessingml/level.rb +6 -20
  158. data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
  159. data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
  160. data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
  161. data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
  162. data/lib/uniword/wordprocessingml/rsids.rb +0 -2
  163. data/lib/uniword/wordprocessingml/run.rb +3 -0
  164. data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
  165. data/lib/uniword/wordprocessingml/settings.rb +8 -34
  166. data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
  167. data/lib/uniword/wordprocessingml/style.rb +2 -1
  168. data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
  169. data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
  170. data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
  171. data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
  172. data/lib/uniword/wordprocessingml.rb +4 -0
  173. data/lib/uniword.rb +27 -2
  174. metadata +38 -19
  175. data/config/validation_rules.yml +0 -60
  176. data/config/warning_rules.yml +0 -57
  177. data/lib/uniword/validation/document_validator.rb +0 -272
  178. data/lib/uniword/validation/structural_validator.rb +0 -116
  179. data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
  180. data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
  181. data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
  182. data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
  183. data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
  184. data/lib/uniword/validators/element_validator.rb +0 -93
  185. data/lib/uniword/validators/paragraph_validator.rb +0 -116
  186. data/lib/uniword/validators/table_validator.rb +0 -134
  187. data/lib/uniword/validators.rb +0 -9
  188. data/lib/uniword/warnings/warning.rb +0 -130
  189. data/lib/uniword/warnings/warning_collector.rb +0 -234
  190. data/lib/uniword/warnings/warning_report.rb +0 -159
  191. data/lib/uniword/warnings.rb +0 -9
@@ -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.
@@ -194,6 +194,14 @@ module Uniword
194
194
  document.custom_xml_items = package.custom_xml_items if package.custom_xml_items
195
195
  document.footnotes = package.footnotes if package.footnotes
196
196
  document.endnotes = package.endnotes if package.endnotes
197
+ document.comments = package.comments if package.comments
198
+ document.settings_rels = package.settings_rels if package.settings_rels
199
+ if package.footnotes_rels
200
+ document.footnotes_rels = package.footnotes_rels
201
+ end
202
+ if package.endnotes_rels
203
+ document.endnotes_rels = package.endnotes_rels
204
+ end
197
205
  end
198
206
 
199
207
  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,83 @@
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
+ # Hash-style read compatibility.
69
+ #
70
+ # @param key [Symbol, String] one of :index, :xml_content,
71
+ # :props_xml, :rels_xml
72
+ # @return [Object, nil]
73
+ def [](key)
74
+ case key.to_sym
75
+ when :index then index
76
+ when :xml_content then xml_content
77
+ when :props_xml then props_xml
78
+ when :rels_xml then rels_xml
79
+ end
80
+ end
81
+ end
82
+ end
83
+ 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
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # Ordered collection of HeaderFooterPart objects — the single
6
+ # storage path for all header/footer parts of a document, whether
7
+ # loaded from an existing DOCX or added programmatically.
8
+ #
9
+ # The +headers+/+footers+ views (HeaderFooterView) and the
10
+ # serializer both read from this store, so a part exists exactly
11
+ # once: one part file, one relationship, one content-type
12
+ # override, one sectPr reference.
13
+ class HeaderFooterPartCollection
14
+ include Enumerable
15
+
16
+ def initialize
17
+ @parts = []
18
+ end
19
+
20
+ # Iterate over all parts (both kinds) in insertion order.
21
+ def each(&block)
22
+ @parts.each(&block)
23
+ end
24
+
25
+ # @return [Integer]
26
+ def size
27
+ @parts.size
28
+ end
29
+
30
+ alias length size
31
+
32
+ # @return [Boolean]
33
+ def empty?
34
+ @parts.empty?
35
+ end
36
+
37
+ # @return [Array<HeaderFooterPart>]
38
+ def to_a
39
+ @parts.dup
40
+ end
41
+
42
+ # Append a part.
43
+ #
44
+ # @param part [HeaderFooterPart]
45
+ # @return [HeaderFooterPart] the added part
46
+ def <<(part)
47
+ @parts << part
48
+ part
49
+ end
50
+
51
+ alias add <<
52
+
53
+ # Remove a part (by identity).
54
+ #
55
+ # @param part [HeaderFooterPart]
56
+ # @return [HeaderFooterPart, nil] the removed part
57
+ def delete(part)
58
+ @parts.delete(part)
59
+ end
60
+
61
+ # @param kind [Symbol] :header or :footer
62
+ # @return [Array<HeaderFooterPart>] parts of that kind, in order
63
+ def of_kind(kind)
64
+ @parts.select { |part| part.kind == kind }
65
+ end
66
+
67
+ # @param kind [Symbol] :header or :footer
68
+ # @param type [String, nil] sectPr reference type
69
+ # @return [HeaderFooterPart, nil]
70
+ def find_part(kind, type)
71
+ type = type&.to_s
72
+ @parts.find { |part| part.kind == kind && part.type == type }
73
+ end
74
+
75
+ # All non-nil relationship targets in the store.
76
+ #
77
+ # @return [Array<String>]
78
+ def targets
79
+ @parts.filter_map(&:target)
80
+ end
81
+
82
+ # First unused numbered target for a kind
83
+ # ("header3.xml" when header1/header2 exist).
84
+ #
85
+ # @param kind [Symbol] :header or :footer
86
+ # @return [String]
87
+ def next_target(kind)
88
+ numbers = of_kind(kind).filter_map do |part|
89
+ part.target&.[](/\A#{kind}(\d+)\.xml\z/, 1)&.to_i
90
+ end
91
+ "#{kind}#{(numbers.max || 0) + 1}.xml"
92
+ end
93
+
94
+ # Replace the entire store (legacy +header_footer_parts=+
95
+ # assignment). Accepts HeaderFooterPart objects and legacy
96
+ # part hashes ({ r_id:, target:, rel_type:, content_type:,
97
+ # content: }).
98
+ #
99
+ # @param parts [Array<HeaderFooterPart, Hash>]
100
+ # @return [void]
101
+ def replace_all(parts)
102
+ @parts = Array(parts).map { |part| self.class.wrap(part) }
103
+ end
104
+
105
+ # Replace all parts of one kind, leaving the other untouched.
106
+ #
107
+ # @param kind [Symbol] :header or :footer
108
+ # @param parts [Array<HeaderFooterPart>]
109
+ # @return [void]
110
+ def replace_kind(kind, parts)
111
+ @parts.reject! { |part| part.kind == kind }
112
+ parts.each { |part| @parts << part }
113
+ end
114
+
115
+ # Normalize one entry into a HeaderFooterPart. Legacy hashes
116
+ # describe loaded parts (they carry original rIds/targets).
117
+ #
118
+ # @param value [HeaderFooterPart, Hash]
119
+ # @return [HeaderFooterPart]
120
+ def self.wrap(value)
121
+ return value if value.is_a?(HeaderFooterPart)
122
+
123
+ HeaderFooterPart.new(
124
+ r_id: value[:r_id],
125
+ target: value[:target],
126
+ rel_type: value[:rel_type],
127
+ content_type: value[:content_type],
128
+ type: value[:type],
129
+ content: value[:content],
130
+ loaded: true,
131
+ )
132
+ end
133
+ end
134
+ end
135
+ end