Class: Uniword::Docx::Package

Inherits:
Lutaml::Model::Serializable
  • Object
show all
Includes:
PackageDefaults, PackageSerialization
Defined in:
lib/uniword/docx/package.rb

Overview

DOCX Package - Complete DOCX file format model

Represents the entire .docx file structure as a lutaml-model object. Each XML file within the ZIP is a separate lutaml-model class.

A DOCX package CONTAINS OOXML markup wrapped in an OPC ZIP container. This class lives in Uniword::Docx, not Uniword::Ooxml, because DOCX is a file format that uses OOXML, not the other way around.

Examples:

Load DOCX

package = Package.from_file('document.docx')
package.core_properties.title = 'New Title'
package.to_file('output.docx')

Access document content

package = Package.from_file('document.docx')
package.document.body.paragraphs.each { |p| puts p.text }

Constant Summary

Constants included from PackageSerialization

Uniword::Docx::PackageSerialization::DOCX_INFRA_OPTIONS, Uniword::Docx::PackageSerialization::DOCX_PART_OPTIONS, Uniword::Docx::PackageSerialization::DOCX_PROPS_OPTIONS, Uniword::Docx::PackageSerialization::DOCX_XML_OPTIONS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from PackageSerialization

#add_standalone, #inject_part_relationships, #serialize_infrastructure, #serialize_package_parts, #serialize_part

Methods included from PackageDefaults

included

Instance Attribute Details

#allocatorObject

Central ID allocator — owns all rId, footnote, bookmark, etc. assignment. Seeded from template rels on load; used by builders during construction.



130
131
132
# File 'lib/uniword/docx/package.rb', line 130

def allocator
  @allocator
end

#custom_xml_itemsArray<CustomXmlItem>?

Custom XML data items (customXml/item*.xml)

Returns:



49
50
51
# File 'lib/uniword/docx/package.rb', line 49

def custom_xml_items
  @custom_xml_items
end

#endnotes_relsObject

Returns the value of attribute endnotes_rels.



98
99
100
# File 'lib/uniword/docx/package.rb', line 98

def endnotes_rels
  @endnotes_rels
end

#footnotes_relsObject

Returns the value of attribute footnotes_rels.



98
99
100
# File 'lib/uniword/docx/package.rb', line 98

def footnotes_rels
  @footnotes_rels
end

#modified_part_pathsObject

Paths that have been modified and should not use raw XML passthrough.



138
139
140
# File 'lib/uniword/docx/package.rb', line 138

def modified_part_paths
  @modified_part_paths
end

#profileObject

Non-serialized attributes (DOCX packaging helpers)



97
98
99
# File 'lib/uniword/docx/package.rb', line 97

def profile
  @profile
end

#raw_xml_partsObject

Raw XML from template ZIP for unmodified parts. When present, these are used verbatim instead of re-serializing through lutaml-model (which drops unmapped elements).



135
136
137
# File 'lib/uniword/docx/package.rb', line 135

def raw_xml_parts
  @raw_xml_parts
end

#settings_relsObject

Returns the value of attribute settings_rels.



98
99
100
# File 'lib/uniword/docx/package.rb', line 98

def settings_rels
  @settings_rels
end

Class Method Details

.from_file(path) ⇒ Package

Load DOCX package from file

Parameters:

  • path (String)

    Path to .docx file

Returns:

  • (Package)

    Package with all parts loaded



176
177
178
179
180
181
182
# File 'lib/uniword/docx/package.rb', line 176

def self.from_file(path)
  extractor = Infrastructure::ZipExtractor.new
  zip_content = extractor.extract(path)
  package = from_zip_content(zip_content, path)
  package.populate_allocator
  package
end

.from_zip_content(zip_content, zip_path = nil) ⇒ Package

Create package from extracted ZIP content

Parts load by iterating Ooxml::PartRegistry.loadable and dispatching each definition to its Docx::PartLoader strategy — see PartLoader for the load order and strategy registry.

Parameters:

  • zip_content (Hash)

    Extracted ZIP files

  • zip_path (String, nil) (defaults to: nil)

    Original ZIP path for binary re-extraction

Returns:



193
194
195
196
197
# File 'lib/uniword/docx/package.rb', line 193

def self.from_zip_content(zip_content, zip_path = nil)
  package = new
  PartLoader.load(zip_content, package, zip_path: zip_path)
  package
end

.to_file(document, path, profile: nil, validate: nil) ⇒ void

This method returns an undefined value.

Save document to file (class method for DocumentWriter compatibility)

Parameters:

  • document (Wordprocessingml::DocumentRoot)

    Document to save

  • path (String)

    Output file path

  • profile (Profile, nil) (defaults to: nil)

    Reconciliation profile

  • validate (Boolean, nil) (defaults to: nil)

    Run the package integrity gate before writing; nil falls back to Uniword.configuration.validate_on_save



207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/uniword/docx/package.rb', line 207

def self.to_file(document, path, profile: nil, validate: nil)
  package = new
  package.document = document
  package.profile = profile || Profile.defaults
  copy_document_parts_to_package(document, package)
  package.content_types ||= minimal_content_types
  package.package_rels ||= minimal_package_rels
  package.document_rels ||= minimal_document_rels
  package.settings ||= Uniword::Wordprocessingml::Settings.new
  package.font_table ||= Uniword::Wordprocessingml::FontTable.new
  package.web_settings ||= Uniword::Wordprocessingml::WebSettings.new
  package.to_file(path, validate: validate)
end

Instance Method Details

#add_stripped_part(path:, reason:) ⇒ void

This method returns an undefined value.

Record a stripped part. Append-only; used by the loader.

Parameters:

  • path (String)

    package-relative path

  • reason (String)

    why the part was stripped (from JunkClassifier#reason)



168
169
170
# File 'lib/uniword/docx/package.rb', line 168

def add_stripped_part(path:, reason:)
  stripped_parts << StrippedPart.new(path: path, reason: reason)
end

#applied_fixesArray<Reconciler::Fix>

Audit trail of repairs applied by the Reconciler during the most recent save (to_zip_content). Empty before the first save and when the package needed no repairs.

Returns:



145
146
147
# File 'lib/uniword/docx/package.rb', line 145

def applied_fixes
  @applied_fixes ||= []
end

#bodyObject



320
321
322
# File 'lib/uniword/docx/package.rb', line 320

def body
  document&.body
end

#chartsObject



334
335
336
# File 'lib/uniword/docx/package.rb', line 334

def charts
  document&.charts || []
end

#each_paragraphObject



328
329
330
# File 'lib/uniword/docx/package.rb', line 328

def each_paragraph(&)
  paragraphs.each(&)
end

#embeddingsPartCollection

OLE/embedded object binaries (word/embeddings/*), keyed by target.

Returns:



103
104
105
# File 'lib/uniword/docx/package.rb', line 103

def embeddings
  @embeddings ||= PartCollection.new(:target, Part)
end

#embeddings=(value) ⇒ Object

Bulk-assign embeddings (Hash of target => Part/binary; nil clears).



108
109
110
# File 'lib/uniword/docx/package.rb', line 108

def embeddings=(value)
  embeddings.replace_all(value)
end

#paragraphsObject

Delegate common DocumentRoot methods for API compatibility



312
313
314
# File 'lib/uniword/docx/package.rb', line 312

def paragraphs
  document&.paragraphs || []
end

#populate_allocatorObject

Populate the allocator from all existing template data. Must be called BEFORE any builder runs (populate-first principle).



223
224
225
# File 'lib/uniword/docx/package.rb', line 223

def populate_allocator
  @allocator = IdAllocator.populate_from_package(self)
end

#prepare_allocatorObject

Guarantee the single rId authority before reconciliation: reuse the package's allocator (loaded/builder-seeded) or start one, then seed it from the package's current relationships. Seeding preserves loaded rIds verbatim; earlier builder allocations keep their ids — a seeded rel whose id collides is reallocated deterministically (IdAllocator#seed_from_rels).



233
234
235
236
237
238
239
# File 'lib/uniword/docx/package.rb', line 233

def prepare_allocator
  self.allocator ||= IdAllocator.new
  allocator.seed_from_rels(package_rels&.relationships,
                           scope: :package)
  allocator.seed_from_rels(document_rels&.relationships)
  allocator
end

#raw_partsPartCollection

Raw passthrough parts no registry definition models, keyed by package path ("docProps/meta.xml", "word/glossary/document.xml", ...). Carried byte-for-byte from load to save — see PartLoader::RawPartLoader for claiming and PackageSerialization#serialize_raw_parts for emission.

Returns:



119
120
121
# File 'lib/uniword/docx/package.rb', line 119

def raw_parts
  @raw_parts ||= PartCollection.new(:path, RawPart)
end

#raw_parts=(value) ⇒ Object

Bulk-assign raw parts (Hash of path => RawPart/hash; nil clears).



124
125
126
# File 'lib/uniword/docx/package.rb', line 124

def raw_parts=(value)
  raw_parts.replace_all(value)
end

#reorder_content_hash(content) ⇒ Object

Ensure [Content_Types].xml is first, _rels/.rels is second.



303
304
305
306
307
308
# File 'lib/uniword/docx/package.rb', line 303

def reorder_content_hash(content)
  priority = {}
  priority["[Content_Types].xml"] = content.delete("[Content_Types].xml") if content.key?("[Content_Types].xml")
  priority["_rels/.rels"] = content.delete("_rels/.rels") if content.key?("_rels/.rels")
  content.replace(priority.merge(content))
end

#stripped_partsArray<StrippedPart>

Parts stripped at load because they were non-compliant (no content type declaration, OS artifact, ...). Populated by Docx::PartLoader::RawPartLoader when Uniword.configuration.on_noncompliant_content is :strip (the default). Empty in :raise mode and for packages constructed by hand.

Returns:

  • (Array<StrippedPart>)

    path + reason for every stripped part, in load order



158
159
160
# File 'lib/uniword/docx/package.rb', line 158

def stripped_parts
  @stripped_parts ||= []
end

#styles_configurationObject



338
339
340
# File 'lib/uniword/docx/package.rb', line 338

def styles_configuration
  document&.styles_configuration
end

#tablesObject



316
317
318
# File 'lib/uniword/docx/package.rb', line 316

def tables
  document&.tables || []
end

#textObject



324
325
326
# File 'lib/uniword/docx/package.rb', line 324

def text
  document&.text || ""
end

#to_file(path, validate: nil) ⇒ void Also known as: save

This method returns an undefined value.

Save package to file

Parameters:

  • path (String)

    Output file path

  • validate (Boolean, nil) (defaults to: nil)

    Run the package integrity gate before writing; nil falls back to Uniword.configuration.validate_on_save

Raises:



249
250
251
252
253
# File 'lib/uniword/docx/package.rb', line 249

def to_file(path, validate: nil)
  zip_content = to_zip_content(validate: validate)
  packager = Infrastructure::ZipPackager.new
  packager.package(zip_content, path)
end

#to_zip_content(validate: nil) ⇒ Hash

Generate ZIP content hash

Runs the Reconciler (the only mutating pass), records its repair report on #applied_fixes, and — unless validation is disabled — refuses invalid output via the PackageIntegrityChecker gate.

Parameters:

  • validate (Boolean, nil) (defaults to: nil)

    Run the package integrity gate; nil falls back to Uniword.configuration.validate_on_save

Returns:

  • (Hash)

    File paths => content

Raises:



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/uniword/docx/package.rb', line 266

def to_zip_content(validate: nil)
  content = {}

  self.content_types ||= self.class.minimal_content_types
  self.package_rels ||= self.class.minimal_package_rels
  self.document_rels ||= self.class.minimal_document_rels

  self.settings ||= Uniword::Wordprocessingml::Settings.new
  self.font_table ||= Uniword::Wordprocessingml::FontTable.new
  self.web_settings ||= Uniword::Wordprocessingml::WebSettings.new

  # An allocator carried into the save (builder-managed document
  # or loaded package) selects light-touch repairs; documents
  # without one get the full normalization repertoire. rIds flow
  # through the allocator either way.
  builder_managed = !allocator.nil?
  prepare_allocator

  reconciler = Reconciler.new(self,
                              profile: profile || Profile.defaults,
                              allocator: allocator,
                              builder_managed: builder_managed)
  reconciler.reconcile
  @applied_fixes = reconciler.applied_fixes
  log_applied_fixes

  inject_part_relationships(content, content_types, package_rels, document_rels)
  serialize_package_parts(content, content_types, package_rels, document_rels)

  # OOXML requires [Content_Types].xml as the first ZIP entry.
  reorder_content_hash(content)

  enforce_package_integrity(content, validate)
  content
end