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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +90 -0
- data/lib/uniword/builder/chart_builder.rb +3 -5
- data/lib/uniword/builder/image_builder.rb +5 -5
- data/lib/uniword/document_factory.rb +10 -24
- data/lib/uniword/docx/custom_xml_item.rb +8 -0
- data/lib/uniword/docx/image_part.rb +83 -0
- data/lib/uniword/docx/package.rb +22 -402
- data/lib/uniword/docx/package_defaults.rb +36 -32
- data/lib/uniword/docx/package_serialization.rb +46 -7
- data/lib/uniword/docx/part.rb +19 -0
- data/lib/uniword/docx/part_collection.rb +15 -7
- data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
- data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
- data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
- data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
- data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
- data/lib/uniword/docx/part_loader/load_context.rb +85 -0
- data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
- data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
- data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
- data/lib/uniword/docx/part_loader.rb +92 -0
- data/lib/uniword/docx/raw_part.rb +75 -0
- data/lib/uniword/docx/reconciler/helpers.rb +9 -30
- data/lib/uniword/docx/reconciler/package_structure.rb +4 -5
- data/lib/uniword/docx/reconciler/referential_integrity.rb +4 -79
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/images/image_manager.rb +13 -13
- data/lib/uniword/ooxml/element_order.rb +94 -0
- data/lib/uniword/ooxml/part_definition.rb +164 -6
- data/lib/uniword/ooxml/part_registry.rb +238 -45
- data/lib/uniword/ooxml/relationships/image_relationship.rb +5 -2
- data/lib/uniword/ooxml.rb +1 -0
- data/lib/uniword/toc/toc_generator.rb +6 -7
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
- data/lib/uniword/validation/opc_validator.rb +16 -1
- data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -0
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +29 -220
- data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
- data/lib/uniword/wordprocessingml/endnotes.rb +2 -3
- data/lib/uniword/wordprocessingml/footnotes.rb +2 -3
- data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
- data/lib/uniword/wordprocessingml.rb +1 -2
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee9e7988ad5a17537188aa93af92867049b6c71fc99e00b24b09cde8f81eda15
|
|
4
|
+
data.tar.gz: 57908a45bf89d01a4b0630736678a5a2ff8ea891fe25650229f83b7b94a88b69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|