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 writes to its package path, content type, relationship type, and [Content_Types].xml entry form (Default vs Override). Content type and relationship type literals live ONLY here; consumers (Uniword::ContentTypes, Docx::PackageDefaults, Docx::Reconciler::PackageStructure, 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.

Examples:

Look up a part by key

defn = PartRegistry.find_by_key(:styles)
defn.part_name # => "/word/styles.xml"
defn.rel_type  # => ".../relationships/styles"

Register a custom part kind

PartRegistry.register(
  PartDefinition.new(key: :glossary, path: "word/glossary/document.xml",
                     target: "glossary/document.xml",
                     content_type: "...document.glossary+xml",
                     rel_type: ".../glossaryDocument",
                     kind: :override, rels_scope: :document)
)

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.

[
  # -- 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },

  # -- 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  { 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 },
  # 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 },
  { 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 },
  # customXml items themselves fall under the "xml" Default;
  # only their itemProps parts get an Override.
  { key: :custom_xml_item, kind: :none,
    path_pattern: "customXml/item%<index>d.xml" },
  { 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 },
].freeze

Class Method Summary collapse

Class Method Details

.allArray<PartDefinition>

Returns all definitions, in registration order (a copy; safe to mutate).

Returns:

  • (Array<PartDefinition>)

    all definitions, in registration order (a copy; safe to mutate)



212
213
214
# File 'lib/uniword/ooxml/part_registry.rb', line 212

def all
  definitions.dup
end

.default_for(extension) ⇒ PartDefinition?

Find a Default definition by file extension.

Parameters:

  • extension (String)

    e.g. "png"

Returns:



254
255
256
257
258
# File 'lib/uniword/ooxml/part_registry.rb', line 254

def default_for(extension)
  definitions.find do |d|
    d.default? && d.extension == extension.to_s
  end
end

.find_by_content_type(content_type) ⇒ PartDefinition?

Returns first definition with this type.

Parameters:

  • content_type (String)

    MIME content type

Returns:



235
236
237
# File 'lib/uniword/ooxml/part_registry.rb', line 235

def find_by_content_type(content_type)
  definitions.find { |d| d.content_type == content_type }
end

.find_by_key(key) ⇒ PartDefinition?

Parameters:

  • key (Symbol)

    registry key (e.g. :styles)

Returns:



218
219
220
# File 'lib/uniword/ooxml/part_registry.rb', line 218

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).

Parameters:

  • path (String)

    package-relative path

Returns:



228
229
230
231
# File 'lib/uniword/ooxml/part_registry.rb', line 228

def find_by_path(path)
  normalized = path.to_s.delete_prefix("/")
  definitions.find { |d| path_match?(d, normalized) }
end

.override_for(part_name) ⇒ PartDefinition?

Find an Override definition by content-type part name.

Parameters:

  • part_name (String)

    e.g. "/word/styles.xml"

Returns:



243
244
245
246
247
248
# File 'lib/uniword/ooxml/part_registry.rb', line 243

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_typesArray<String>

Relationship type URIs whose relationships belong in the package-level _rels/.rels part.

Returns:

  • (Array<String>)

    in registration order



277
278
279
# File 'lib/uniword/ooxml/part_registry.rb', line 277

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.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when not given a PartDefinition



186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/uniword/ooxml/part_registry.rb', line 186

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_defaultsArray<PartDefinition>

Definitions included in the comprehensive [Content_Types].xml that ContentTypes.generate emits for new DOCX packages.

Returns:



264
265
266
# File 'lib/uniword/ooxml/part_registry.rb', line 264

def standard_defaults
  definitions.select { |d| d.standard? && d.default? }
end

.standard_overridesArray<PartDefinition>

Returns in registration order.

Returns:



269
270
271
# File 'lib/uniword/ooxml/part_registry.rb', line 269

def standard_overrides
  definitions.select { |d| d.standard? && d.override? }
end

.unregister(key) ⇒ Array<PartDefinition>?

Remove a registered definition (primarily for tests).

Parameters:

  • key (Symbol)

    registry key

Returns:

  • (Array<PartDefinition>, nil)

    the definitions list if a definition was removed, nil when the key was absent



206
207
208
# File 'lib/uniword/ooxml/part_registry.rb', line 206

def unregister(key)
  definitions.reject! { |d| d.key == key.to_sym }
end