uniword 1.3.0 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/lib/uniword/builder/chart_builder.rb +3 -5
  4. data/lib/uniword/builder/image_builder.rb +5 -5
  5. data/lib/uniword/document_factory.rb +10 -24
  6. data/lib/uniword/docx/custom_xml_item.rb +8 -0
  7. data/lib/uniword/docx/image_part.rb +83 -0
  8. data/lib/uniword/docx/package.rb +22 -402
  9. data/lib/uniword/docx/package_defaults.rb +36 -32
  10. data/lib/uniword/docx/package_serialization.rb +46 -7
  11. data/lib/uniword/docx/part.rb +19 -0
  12. data/lib/uniword/docx/part_collection.rb +15 -7
  13. data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
  14. data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
  15. data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
  16. data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
  17. data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
  18. data/lib/uniword/docx/part_loader/load_context.rb +85 -0
  19. data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
  20. data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
  21. data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
  22. data/lib/uniword/docx/part_loader.rb +92 -0
  23. data/lib/uniword/docx/raw_part.rb +75 -0
  24. data/lib/uniword/docx/reconciler/helpers.rb +9 -30
  25. data/lib/uniword/docx/reconciler/package_structure.rb +4 -5
  26. data/lib/uniword/docx/reconciler/referential_integrity.rb +4 -79
  27. data/lib/uniword/docx.rb +3 -0
  28. data/lib/uniword/images/image_manager.rb +13 -13
  29. data/lib/uniword/ooxml/element_order.rb +94 -0
  30. data/lib/uniword/ooxml/part_definition.rb +164 -6
  31. data/lib/uniword/ooxml/part_registry.rb +238 -45
  32. data/lib/uniword/ooxml/relationships/image_relationship.rb +5 -2
  33. data/lib/uniword/ooxml.rb +1 -0
  34. data/lib/uniword/toc/toc_generator.rb +6 -7
  35. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
  36. data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
  37. data/lib/uniword/validation/opc_validator.rb +16 -1
  38. data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -0
  39. data/lib/uniword/version.rb +1 -1
  40. data/lib/uniword/wordprocessingml/document_root.rb +29 -220
  41. data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
  42. data/lib/uniword/wordprocessingml/endnotes.rb +2 -3
  43. data/lib/uniword/wordprocessingml/footnotes.rb +2 -3
  44. data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
  45. data/lib/uniword/wordprocessingml.rb +1 -2
  46. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c28f3b26ea127104f901589ebdb8752163be6f9e963c11eca94a26c86bdbd0df
4
- data.tar.gz: d8fbe7de4f8f005d78d10acd4f6c1f7459d06385b419c487732e0398470d2c23
3
+ metadata.gz: ee9e7988ad5a17537188aa93af92867049b6c71fc99e00b24b09cde8f81eda15
4
+ data.tar.gz: 57908a45bf89d01a4b0630736678a5a2ff8ea891fe25650229f83b7b94a88b69
5
5
  SHA512:
6
- metadata.gz: 807f859be7a5b6b45512ea946e4a4b1bd12a8e9df975e9dd9e06dd75c0842d916e5f811e9ac10062873426402b0a3526fa5301a0634e15062fb3f65696f3bd03
7
- data.tar.gz: 3d6d287a816d2c2cc42a59b5da669dc92026774b0a2e27ff02e33a916367a63336e390338e556d1688b6cb7bbf78676dd5f200b47e8fdcd5c1509e0ebd238f90
6
+ metadata.gz: e7b6660cbc36ccff3170559c4c8eebaeed56a2face7fba85ae5ec1c42ad09ba22270fe9d0b5cabc82be78a5cfc29dfa67cb8caf32afc976bd0c7d8530dd358db
7
+ data.tar.gz: b1d88957cf0b8c3b647246afb81d3e61e3a9e6c327115994b69f0a0c7070754380dafbd3e9a7ef7cf0379821d741de190f0fce58a04db8f86b1dcecd3095286e
data/CHANGELOG.md CHANGED
@@ -8,6 +8,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.4.0] - 2026-07-20
12
+
13
+ ### Added
14
+
15
+ - DOCX parts the library does not model (e.g. `docProps/meta.xml`,
16
+ glossary documents, VBA projects, customXml outside the standard
17
+ layout, header/comment `.rels` sidecars) now round-trip
18
+ byte-for-byte instead of being dropped on load. Unclaimed ZIP
19
+ entries are preserved as raw parts (`Docx::RawPart`,
20
+ `Package#raw_parts`) with their source content types, re-emitted
21
+ verbatim on save; relationships targeting them are kept by the
22
+ reconciler's dangling-relationship sweep, and the write-time
23
+ package integrity gate treats them as first-class parts.
24
+
25
+ ### Changed
26
+
27
+ - Internal refactor (no user-visible behavior change): `Ooxml::PartRegistry`
28
+ is now bidirectional and loader-aware. `Ooxml::PartDefinition` describes
29
+ how each part loads (loader strategy key, parsing model, load priority,
30
+ dynamic path resolution) and where it lives on
31
+ `Wordprocessingml::DocumentRoot` and `Docx::Package`. Registry
32
+ registrations now cover every part the library reads (content types,
33
+ package/document/settings/theme/footnote/endnote rels, theme media).
34
+ `Docx::Package.from_zip_content` loads parts by iterating registry
35
+ entries through the new `Docx::PartLoader` strategy registry
36
+ (`XmlModelLoader`, `CustomXmlLoader`, `HeaderFooterLoader`,
37
+ `ChartLoader`, `ImageLoader`, `EmbeddingLoader`, `ThemeMediaLoader`)
38
+ instead of a hand-written per-part sequence, and the two
39
+ document↔package copy lists
40
+ (`DocumentFactory.copy_package_parts_to_document`,
41
+ `Docx::PackageDefaults.copy_document_parts_to_package`) are single
42
+ registry-driven loops — a part can no longer exist in only one
43
+ direction.
44
+ - Internal refactor (no user-visible behavior change): the remaining
45
+ hash-based part families are now model objects. New
46
+ `Docx::ImagePart` (binary data, target, content type, relationship
47
+ id, source path) backs `DocumentRoot#image_parts`, which is now an
48
+ rId-keyed `Docx::PartCollection` that normalizes legacy
49
+ `{ data:, target:, content_type: }` hash assignments and keeps
50
+ hash-style reads working (`[]`, `each`, `each_value`, `keys`,
51
+ `size`, `empty?`, `[]=`, `delete`, `key?`/`has_key?`).
52
+ `Docx::PartCollection` wraps hash entries through the new
53
+ polymorphic `Docx::Part.from_hash` hook. The write-only
54
+ `Package#chart_parts` and `Package#bibliography_sources` copies are
55
+ gone — `DocumentRoot#chart_parts` and
56
+ `DocumentRoot#bibliography_sources` are the single homes, and the
57
+ registry's `:chart`/`:bibliography` definitions no longer carry
58
+ document/package copy attributes. Builders, part loaders,
59
+ serializers and reconcilers read and write the model objects
60
+ directly.
61
+ - Internal refactor (no user-visible behavior change): "will this part
62
+ be emitted?" is now answered by `Ooxml::PartRegistry.emitted_paths`
63
+ instead of a hand-written mirror in the reconciler. Part objects
64
+ (`Docx::Part`, `Docx::CustomXmlItem`) report their own emitted
65
+ package paths (`package_paths`), and family definitions
66
+ (`:header`/`:footer`/`:chart`/`:image`/`:bibliography`) gained the
67
+ collection attributes needed to enumerate them.
68
+ - Internal refactor (no user-visible behavior change):
69
+ `Uniword::Ooxml::ElementOrder` is now the single safe
70
+ `element_order` mutation point (`mutable_order`, `append`,
71
+ `insert_at`, `insert_once`), replacing per-model workarounds for
72
+ lutaml-model's frozen parse arrays in footnotes, endnotes, the TOC
73
+ generator, and the reconciler helpers
74
+
75
+ ### Fixed
76
+
77
+ - `uniword verify` no longer reports false OPC-006 "relationship
78
+ target not found" errors for relationship targets containing dot
79
+ segments (e.g. `../docProps/meta.xml`); target resolution now
80
+ normalizes `.`/`..` per OPC Part 2, matching the write-time gate.
81
+ - `uniword verify --xsd` no longer drowns in false errors for MCE
82
+ extension content every real document carries: the XSD layer
83
+ preprocesses each part per its own `mc:Ignorable` declaration,
84
+ stripping ignorable-marked attributes (w14:paraId/textId) and the
85
+ `mc:Ignorable` attribute itself before validation. Extension elements
86
+ are deliberately kept (removing them exposed baseline Word-vs-schema
87
+ quirks as false errors); their findings remain classified as
88
+ MCE-attributable
89
+
90
+ ### Changed
91
+
92
+ - `DocumentRoot#remove_style` now returns the removed `Style` object or
93
+ nil (previously a Boolean), consistent with `#rename_style`; the
94
+ theme/style/scheme application methods on `DocumentRoot` moved into
95
+ `Wordprocessingml::DocumentStyling` (internal extraction, signatures
96
+ unchanged)
97
+ - `Ooxml::Relationships::ImageRelationship.new` now requires an
98
+ explicit `id:` (the unreachable random rId default is gone;
99
+ `Docx::IdAllocator` remains the single rId authority)
100
+
11
101
  ## [1.3.0] - 2026-07-19
12
102
 
13
103
  ### Added
@@ -133,16 +133,14 @@ module Uniword
133
133
  # @param root [Wordprocessingml::DocumentRoot] target document
134
134
  # @return [String] relationship id of the chart part
135
135
  def register_chart_part(root)
136
- root.chart_parts ||= {}
137
136
  root.allocator ||= Docx::IdAllocator.new
138
137
 
139
138
  target = "charts/chart#{root.chart_parts.size + 1}.xml"
140
139
  r_id = root.allocator.alloc_rid(target: target, type: CHART_REL_TYPE)
141
140
 
142
- root.chart_parts[r_id] = {
143
- xml: build_xml,
144
- target: target,
145
- }
141
+ root.chart_parts[r_id] = Docx::ChartPart.new(
142
+ r_id: r_id, target: target, content: build_xml,
143
+ )
146
144
  r_id
147
145
  end
148
146
 
@@ -60,13 +60,13 @@ module Uniword
60
60
  end
61
61
 
62
62
  if root
63
- root.image_parts ||= {}
64
- root.image_parts[r_id] = {
65
- path: path,
63
+ root.image_parts[r_id] = Docx::ImagePart.new(
64
+ r_id: r_id,
66
65
  data: File.binread(path),
67
66
  content_type: content_type,
68
- target: "media/#{File.basename(path)}",
69
- }
67
+ target: target,
68
+ source_path: path,
69
+ )
70
70
  end
71
71
  r_id
72
72
  end
@@ -169,38 +169,24 @@ 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
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
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)
204
190
  end
205
191
  end
206
192
 
@@ -65,6 +65,14 @@ module Uniword
65
65
  .path_for(index: index)
66
66
  end
67
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
+
68
76
  # Hash-style read compatibility.
69
77
  #
70
78
  # @param key [Symbol, String] one of :index, :xml_content,
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # An image part (word/media/*) held by the package: binary image
6
+ # data plus the packaging metadata needed to emit it (relationship
7
+ # target, relationship id, content type).
8
+ #
9
+ # The image content type is per-file (resolved from the file
10
+ # extension), so it is always carried explicitly — the registry's
11
+ # :image definition contributes only the relationship type.
12
+ #
13
+ # Replaces the raw +{ data:, target:, content_type:, path: }+ hash
14
+ # entries formerly stored in +DocumentRoot#image_parts+. Hash-style
15
+ # read access (+part[:data]+, +part[:target]+, +part[:content_type]+,
16
+ # +part[:path]+) is kept for backward compatibility.
17
+ #
18
+ # @example
19
+ # part = ImagePart.new(
20
+ # r_id: "rId5", target: "media/image1.png",
21
+ # data: binary_data, content_type: "image/png",
22
+ # )
23
+ # part.data # => binary image data
24
+ # part.content_type # => "image/png"
25
+ class ImagePart < Part
26
+ # @return [String, nil] path of the source file the image was
27
+ # read from (builder-added images only; nil for loaded parts)
28
+ attr_accessor :source_path
29
+
30
+ # @param r_id [String, nil] relationship id
31
+ # @param target [String, nil] e.g. "media/image1.png"
32
+ # @param data [String, nil] binary image data
33
+ # @param content_type [String, nil] e.g. "image/png"
34
+ # @param source_path [String, nil] on-disk source path
35
+ # rubocop:disable Metrics/ParameterLists
36
+ def initialize(r_id: nil, target: nil, data: nil, content_type: nil,
37
+ source_path: nil, **rest)
38
+ super(
39
+ definition: Ooxml::PartRegistry.find_by_key(:image),
40
+ r_id: r_id, target: target, content: data,
41
+ content_type: content_type, **rest
42
+ )
43
+ @source_path = source_path
44
+ end
45
+ # rubocop:enable Metrics/ParameterLists
46
+
47
+ # Wrap a legacy raw-hash entry ({ data:, target:, content_type:,
48
+ # path: }) into an ImagePart.
49
+ #
50
+ # @param hash [Hash] legacy hash entry
51
+ # @return [ImagePart]
52
+ def self.from_hash(hash)
53
+ new(target: hash[:target], data: hash[:data],
54
+ content_type: hash[:content_type], source_path: hash[:path])
55
+ end
56
+
57
+ # Binary image data (alias for content).
58
+ #
59
+ # @return [String, nil]
60
+ def data
61
+ content
62
+ end
63
+
64
+ # Set the binary image data (alias for content=).
65
+ #
66
+ # @param value [String, nil] binary image data
67
+ # @return [void]
68
+ def data=(value)
69
+ self.content = value
70
+ end
71
+
72
+ # Hash-style read compatibility (+:data+ and the legacy +:path+
73
+ # source path in addition to the Part keys).
74
+ def [](key)
75
+ case key.to_sym
76
+ when :data then content
77
+ when :path then source_path
78
+ else super
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end