Module: Uniword::Ooxml::PartRegistry
- Defined in:
- lib/uniword/ooxml/part_registry.rb
Overview
Single source of truth for OOXML package part metadata.
Maps every part kind the library reads or writes to its package path, content type, relationship type, [Content_Types].xml entry form (Default vs Override), loader strategy, and document/package attributes. Content type and relationship type literals live ONLY here; consumers (Uniword::ContentTypes, Docx::PackageDefaults, Docx::Reconciler::PackageStructure, Docx::PartLoader, DocumentFactory, and the Docx::PackageSerialization inject_* methods) derive from this registry instead of holding their own literals.
Open/closed: a new part kind is added by registering a PartDefinition — consumers need no further changes.
Constant Summary collapse
- OFFICE_REL_BASE =
Relationship type base URIs. These literals appear only here.
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"- PACKAGE_REL_BASE =
"http://schemas.openxmlformats.org/package/2006/relationships"- CT_OFFICE =
Content type prefixes (literals appear only here).
"application/vnd.openxmlformats-officedocument"- CT_WML =
"#{CT_OFFICE}.wordprocessingml".freeze
- CT_PACKAGE =
"application/vnd.openxmlformats-package"- BUILT_INS =
Built-in registrations. Registration order is observable in emitted output: the :standard entries reproduce the historic ContentTypes.generate ordering exactly (defaults first — jpeg, png, gif, rels, xml — then overrides in the historic order). Non-standard parts follow; consumers select their own ordered subsets by key.
Loader metadata:
loaderselects the Docx::PartLoader strategy,loader_modelthe parsing model class,load_priorityorders the load (content types and package rels first; the main document before the parts its rels describe), and +document_attribute+/+package_attribute+ name where the part lives on Wordprocessingml::DocumentRoot and Docx::Package for the two registry-driven copy loops. [ # -- Content-type Defaults (extension → content type) -- { key: :jpeg, kind: :default, extension: "jpeg", content_type: "image/jpeg", standard: true }, { key: :png, kind: :default, extension: "png", content_type: "image/png", standard: true }, { key: :gif, kind: :default, extension: "gif", content_type: "image/gif", standard: true }, { key: :rels, kind: :default, extension: "rels", content_type: "#{CT_PACKAGE}.relationships+xml", required: true, standard: true }, { key: :xml, kind: :default, extension: "xml", content_type: "application/xml", required: true, standard: true }, # -- Standard Override parts (historic generate order) -- { key: :document, kind: :override, path: "word/document.xml", content_type: "#{CT_WML}.document.main+xml", rel_type: "#{OFFICE_REL_BASE}/officeDocument", required: true, standard: true, rels_scope: :package, loader: :xml_model, loader_model: Uniword::Wordprocessingml::DocumentRoot, path_resolution: :office_document, load_priority: 40, package_attribute: :document }, { key: :numbering, kind: :override, path: "word/numbering.xml", target: "numbering.xml", content_type: "#{CT_WML}.numbering+xml", rel_type: "#{OFFICE_REL_BASE}/numbering", standard: true, rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::NumberingConfiguration, load_priority: 50, package_attribute: :numbering, document_attribute: :numbering_configuration, to_package_guard: :numbering_configuration_loaded? }, { key: :styles, kind: :override, path: "word/styles.xml", target: "styles.xml", content_type: "#{CT_WML}.styles+xml", rel_type: "#{OFFICE_REL_BASE}/styles", required: true, standard: true, rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::StylesConfiguration, load_priority: 50, package_attribute: :styles, document_attribute: :styles_configuration }, { key: :settings, kind: :override, path: "word/settings.xml", target: "settings.xml", content_type: "#{CT_WML}.settings+xml", rel_type: "#{OFFICE_REL_BASE}/settings", required: true, standard: true, rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::Settings, load_priority: 50, package_attribute: :settings, document_attribute: :settings }, { key: :web_settings, kind: :override, path: "word/webSettings.xml", target: "webSettings.xml", content_type: "#{CT_WML}.webSettings+xml", rel_type: "#{OFFICE_REL_BASE}/webSettings", required: true, standard: true, rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::WebSettings, load_priority: 50, package_attribute: :web_settings, document_attribute: :web_settings }, { key: :font_table, kind: :override, path: "word/fontTable.xml", target: "fontTable.xml", content_type: "#{CT_WML}.fontTable+xml", rel_type: "#{OFFICE_REL_BASE}/fontTable", required: true, standard: true, rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::FontTable, load_priority: 50, package_attribute: :font_table, document_attribute: :font_table }, { key: :theme, kind: :override, path: "word/theme/theme1.xml", target: "theme/theme1.xml", content_type: "#{CT_OFFICE}.theme+xml", rel_type: "#{OFFICE_REL_BASE}/theme", standard: true, rels_scope: :document, loader: :xml_model, loader_model: Uniword::Drawingml::Theme, load_priority: 70, package_attribute: :theme, document_attribute: :theme }, { key: :core_properties, kind: :override, path: "docProps/core.xml", content_type: "#{CT_PACKAGE}.core-properties+xml", rel_type: "#{PACKAGE_REL_BASE}/metadata/core-properties", required: true, standard: true, rels_scope: :package, loader: :xml_model, loader_model: CoreProperties, load_priority: 30, package_attribute: :core_properties, document_attribute: :core_properties }, { key: :app_properties, kind: :override, path: "docProps/app.xml", content_type: "#{CT_OFFICE}.extended-properties+xml", rel_type: "#{OFFICE_REL_BASE}/extended-properties", required: true, standard: true, rels_scope: :package, loader: :xml_model, loader_model: AppProperties, load_priority: 30, package_attribute: :app_properties, document_attribute: :app_properties }, # -- Optional document parts -- { key: :footnotes, kind: :override, path: "word/footnotes.xml", target: "footnotes.xml", content_type: "#{CT_WML}.footnotes+xml", rel_type: "#{OFFICE_REL_BASE}/footnotes", rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::Footnotes, load_priority: 80, package_attribute: :footnotes, document_attribute: :footnotes }, { key: :endnotes, kind: :override, path: "word/endnotes.xml", target: "endnotes.xml", content_type: "#{CT_WML}.endnotes+xml", rel_type: "#{OFFICE_REL_BASE}/endnotes", rels_scope: :document, loader: :xml_model, loader_model: Uniword::Wordprocessingml::Endnotes, load_priority: 80, package_attribute: :endnotes, document_attribute: :endnotes }, { key: :comments, kind: :override, path: "word/comments.xml", target: "comments.xml", content_type: "#{CT_WML}.comments+xml", rel_type: "#{OFFICE_REL_BASE}/comments", rels_scope: :document, loader: :xml_model, loader_model: Uniword::CommentsPart, load_priority: 80, package_attribute: :comments, document_attribute: :comments, to_package_type: Uniword::CommentsPart }, # Bibliography sources live only on the document (single home: # DocumentRoot#bibliography_sources); nothing reads # word/sources.xml back, so there is no loader. { key: :bibliography, kind: :override, path: "word/sources.xml", target: "sources.xml", content_type: "#{CT_OFFICE}.bibliography+xml", rel_type: "#{OFFICE_REL_BASE}/bibliography", rels_scope: :document, document_attribute: :bibliography_sources }, { key: :header, kind: :override, path_pattern: "word/header%<counter>d.xml", target_pattern: "header%<counter>d.xml", content_type: "#{CT_WML}.header+xml", rel_type: "#{OFFICE_REL_BASE}/header", rels_scope: :document, loader: :header_footer, loader_model: Uniword::Wordprocessingml::Header, load_priority: 90, document_attribute: :header_footer_parts }, { key: :footer, kind: :override, path_pattern: "word/footer%<counter>d.xml", target_pattern: "footer%<counter>d.xml", content_type: "#{CT_WML}.footer+xml", rel_type: "#{OFFICE_REL_BASE}/footer", rels_scope: :document, loader: :header_footer, loader_model: Uniword::Wordprocessingml::Footer, load_priority: 90, document_attribute: :header_footer_parts }, { key: :custom_properties, kind: :override, path: "docProps/custom.xml", content_type: "#{CT_OFFICE}.custom-properties+xml", rel_type: "#{OFFICE_REL_BASE}/custom-properties", rels_scope: :package, loader: :xml_model, loader_model: CustomProperties, load_priority: 30, package_attribute: :custom_properties, document_attribute: :custom_properties }, # The chart loader keys parts by document relationship id, so # they land directly on the document — the single home for # chart parts (DocumentRoot#chart_parts). { key: :chart, kind: :override, path_pattern: "word/charts/chart%<n>d.xml", target_pattern: "charts/chart%<n>d.xml", content_type: "#{CT_OFFICE}.drawingml.chart+xml", rel_type: "#{OFFICE_REL_BASE}/chart", rels_scope: :document, loader: :chart, load_priority: 100, document_attribute: :chart_parts }, # Images carry a per-extension Default content type resolved # from the image data at save time, so content_type is nil. { key: :image, kind: :default, path_pattern: "word/media/%<name>s", target_pattern: "media/%<name>s", rel_type: "#{OFFICE_REL_BASE}/image", rels_scope: :document, loader: :image, load_priority: 110, document_attribute: :image_parts }, # The embedding loader keys parts by target on the package's # embeddings collection; no package→document copy. { key: :ole_object, kind: :override, path_pattern: "word/embeddings/%<name>s", target_pattern: "embeddings/%<name>s", content_type: "#{CT_OFFICE}.oleObject", rel_type: "#{OFFICE_REL_BASE}/oleObject", rels_scope: :document, loader: :embedding, load_priority: 120, package_attribute: :embeddings, document_attribute: :embeddings, copy_to_document: false }, # customXml items themselves fall under the "xml" Default; # only their itemProps parts get an Override. The :custom_xml # loader also pulls each item's itemProps and .rels sidecars. { key: :custom_xml_item, kind: :none, path_pattern: "customXml/item%<index>d.xml", loader: :custom_xml, load_priority: 30, package_attribute: :custom_xml_items, document_attribute: :custom_xml_items }, { key: :custom_xml_item_props, kind: :override, path_pattern: "customXml/itemProps%<index>d.xml", content_type: "#{CT_OFFICE}.customXmlProperties+xml" }, # Hyperlink relationships target external URLs; no part, no # content type. { key: :hyperlink, kind: :none, rel_type: "#{OFFICE_REL_BASE}/hyperlink", rels_scope: :document }, # Theme part of THMX (theme) packages, rooted at /theme. { key: :thmx_theme, kind: :override, path: "theme/theme1.xml", content_type: "#{CT_OFFICE}.theme+xml", required: true }, # -- Read-side infrastructure parts (no content-type entry # of their own; kind :none keeps emission untouched) -- { key: :content_types, kind: :none, path: "[Content_Types].xml", loader: :xml_model, loader_model: Uniword::ContentTypes::Types, load_priority: 10, package_attribute: :content_types, document_attribute: :content_types }, { key: :package_rels, kind: :none, path: "_rels/.rels", loader: :xml_model, loader_model: Relationships::PackageRelationships, load_priority: 20, package_attribute: :package_rels, document_attribute: :package_rels }, { key: :document_rels, kind: :none, path: "word/_rels/document.xml.rels", loader: :xml_model, loader_model: Relationships::PackageRelationships, path_resolution: :office_document_rels, load_priority: 60, package_attribute: :document_rels, document_attribute: :document_rels }, { key: :settings_rels, kind: :none, path: "word/_rels/settings.xml.rels", loader: :xml_model, loader_model: Relationships::PackageRelationships, load_priority: 50, package_attribute: :settings_rels, document_attribute: :settings_rels }, { key: :theme_rels, kind: :none, path: "word/theme/_rels/theme1.xml.rels", loader: :xml_model, loader_model: Relationships::PackageRelationships, load_priority: 75, package_attribute: :theme_rels, document_attribute: :theme_rels }, { key: :footnotes_rels, kind: :none, path: "word/_rels/footnotes.xml.rels", loader: :xml_model, loader_model: Relationships::PackageRelationships, load_priority: 85, package_attribute: :footnotes_rels, document_attribute: :footnotes_rels }, { key: :endnotes_rels, kind: :none, path: "word/_rels/endnotes.xml.rels", loader: :xml_model, loader_model: Relationships::PackageRelationships, load_priority: 85, package_attribute: :endnotes_rels, document_attribute: :endnotes_rels }, # Theme media files live on the theme's media_files, not on a # package/document attribute; loads only when a theme part was # parsed (see PartLoader::ThemeMediaLoader). { key: :theme_media, kind: :none, path_pattern: "word/theme/media/%<name>s", loader: :theme_media, load_priority: 72 }, # Fallback passthrough for parts no definition claims # (unmodelled parts such as docProps/meta.xml, glossary # documents, VBA projects): the :raw loader runs last and # carries the remainder byte-for-byte on the package's # raw_parts collection. Emission is derived from the stored # parts like any collection family, so emitted_paths — and # with it the reconciler's dangling-relationship sweep — sees # raw-carried parts as first-class. { key: :raw_parts, kind: :none, loader: :raw, load_priority: 130, package_attribute: :raw_parts, document_attribute: :raw_parts }, ].freeze
Class Method Summary collapse
-
.all ⇒ Array<PartDefinition>
All definitions, in registration order (a copy; safe to mutate).
-
.copied_to_document ⇒ Array<PartDefinition>
Definitions mirrored package→document by DocumentFactory.copy_package_parts_to_document.
-
.copied_to_package ⇒ Array<PartDefinition>
Definitions mirrored document→package by Docx::PackageDefaults.copy_document_parts_to_package.
-
.default_for(extension) ⇒ PartDefinition?
Find a Default definition by file extension.
-
.emitted_paths(package) ⇒ Set<String>
Package paths the given package will emit, derived from the definitions: single-instance kinds by model presence, collection families by their stored parts.
-
.find_by_content_type(content_type) ⇒ PartDefinition?
First definition with this type.
- .find_by_key(key) ⇒ PartDefinition?
-
.find_by_path(path) ⇒ PartDefinition?
Find by package path; accepts both "word/styles.xml" and "/word/styles.xml" forms, and matches numbered paths against registered patterns ("word/header2.xml" → :header).
-
.loadable ⇒ Array<PartDefinition>
Definitions loadable from extracted ZIP content, in load order: ascending PartDefinition#load_priority, registration order within a priority (explicit index tiebreak — Ruby's sort_by is not stable).
-
.override_for(part_name) ⇒ PartDefinition?
Find an Override definition by content-type part name.
-
.package_rel_types ⇒ Array<String>
Relationship type URIs whose relationships belong in the package-level _rels/.rels part.
-
.register(definition) ⇒ PartDefinition
Register a part definition.
-
.standard_defaults ⇒ Array<PartDefinition>
Definitions included in the comprehensive [Content_Types].xml that ContentTypes.generate emits for new DOCX packages.
-
.standard_overrides ⇒ Array<PartDefinition>
In registration order.
-
.unregister(key) ⇒ Array<PartDefinition>?
Remove a registered definition (primarily for tests).
Class Method Details
.all ⇒ Array<PartDefinition>
Returns all definitions, in registration order (a copy; safe to mutate).
376 377 378 |
# File 'lib/uniword/ooxml/part_registry.rb', line 376 def all definitions.dup end |
.copied_to_document ⇒ Array<PartDefinition>
Definitions mirrored package→document by DocumentFactory.copy_package_parts_to_document.
461 462 463 464 465 466 |
# File 'lib/uniword/ooxml/part_registry.rb', line 461 def copied_to_document definitions.select do |d| d.document_attribute && d.package_attribute && d.copy_to_document? end end |
.copied_to_package ⇒ Array<PartDefinition>
Definitions mirrored document→package by Docx::PackageDefaults.copy_document_parts_to_package.
472 473 474 |
# File 'lib/uniword/ooxml/part_registry.rb', line 472 def copied_to_package definitions.select { |d| d.document_attribute && d.package_attribute } end |
.default_for(extension) ⇒ PartDefinition?
Find a Default definition by file extension.
418 419 420 421 422 |
# File 'lib/uniword/ooxml/part_registry.rb', line 418 def default_for(extension) definitions.find do |d| d.default? && d.extension == extension.to_s end end |
.emitted_paths(package) ⇒ Set<String>
Package paths the given package will emit, derived from the definitions: single-instance kinds by model presence, collection families by their stored parts.
Single authority for "will this part be emitted?" — used by the reconciler's dangling-relationship sweep.
485 486 487 488 |
# File 'lib/uniword/ooxml/part_registry.rb', line 485 def emitted_paths(package) all.flat_map { |definition| definition.emitted_paths(package) } .compact.to_set end |
.find_by_content_type(content_type) ⇒ PartDefinition?
Returns first definition with this type.
399 400 401 |
# File 'lib/uniword/ooxml/part_registry.rb', line 399 def find_by_content_type(content_type) definitions.find { |d| d.content_type == content_type } end |
.find_by_key(key) ⇒ PartDefinition?
382 383 384 |
# File 'lib/uniword/ooxml/part_registry.rb', line 382 def find_by_key(key) definitions.find { |d| d.key == key.to_sym } end |
.find_by_path(path) ⇒ PartDefinition?
Find by package path; accepts both "word/styles.xml" and "/word/styles.xml" forms, and matches numbered paths against registered patterns ("word/header2.xml" → :header).
392 393 394 395 |
# File 'lib/uniword/ooxml/part_registry.rb', line 392 def find_by_path(path) normalized = path.to_s.delete_prefix("/") definitions.find { |d| path_match?(d, normalized) } end |
.loadable ⇒ Array<PartDefinition>
Definitions loadable from extracted ZIP content, in load order: ascending PartDefinition#load_priority, registration order within a priority (explicit index tiebreak — Ruby's sort_by is not stable).
451 452 453 454 455 |
# File 'lib/uniword/ooxml/part_registry.rb', line 451 def loadable definitions.select(&:loadable?).each_with_index .sort_by { |d, i| [d.load_priority || 100, i] } .map(&:first) end |
.override_for(part_name) ⇒ PartDefinition?
Find an Override definition by content-type part name.
407 408 409 410 411 412 |
# File 'lib/uniword/ooxml/part_registry.rb', line 407 def override_for(part_name) normalized = part_name.to_s.delete_prefix("/") definitions.find do |d| d.override? && path_match?(d, normalized) end end |
.package_rel_types ⇒ Array<String>
Relationship type URIs whose relationships belong in the package-level _rels/.rels part.
441 442 443 |
# File 'lib/uniword/ooxml/part_registry.rb', line 441 def package_rel_types definitions.filter_map { |d| d.rel_type if d.package_rel? } end |
.register(definition) ⇒ PartDefinition
Register a part definition. Re-registering an existing key replaces it in place (preserving position); new keys append.
350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/uniword/ooxml/part_registry.rb', line 350 def register(definition) unless definition.is_a?(PartDefinition) raise ArgumentError, "expected PartDefinition, got #{definition.class}" end index = definitions.index { |d| d.key == definition.key } if index definitions[index] = definition else definitions << definition end definition end |
.standard_defaults ⇒ Array<PartDefinition>
Definitions included in the comprehensive [Content_Types].xml that ContentTypes.generate emits for new DOCX packages.
428 429 430 |
# File 'lib/uniword/ooxml/part_registry.rb', line 428 def standard_defaults definitions.select { |d| d.standard? && d.default? } end |
.standard_overrides ⇒ Array<PartDefinition>
Returns in registration order.
433 434 435 |
# File 'lib/uniword/ooxml/part_registry.rb', line 433 def standard_overrides definitions.select { |d| d.standard? && d.override? } end |
.unregister(key) ⇒ Array<PartDefinition>?
Remove a registered definition (primarily for tests).
370 371 372 |
# File 'lib/uniword/ooxml/part_registry.rb', line 370 def unregister(key) definitions.reject! { |d| d.key == key.to_sym } end |