Class: Uniword::Docx::IdAllocator

Inherits:
Object
  • Object
show all
Defined in:
lib/uniword/docx/id_allocator.rb

Overview

Single owner of all ID assignment in a DOCX package.

Every builder, the adapter, and the reconciler call into this class for rId, footnote, endnote, bookmark, comment, and paragraph ID allocation. No other code generates IDs.

Two usage modes:

Creating from scratch — allocator starts empty, all IDs are fresh.
Editing a template   — call seed_from_* methods BEFORE any builder
                      runs, so new IDs don't collide with existing ones.

This is the "populate-first" principle: when loading a template DOCX, parse and seed ALL existing IDs from the template before modification.

rId namespaces are per relationships part: "rId1" in _rels/.rels and "rId1" in word/_rels/document.xml.rels do not collide. The allocator therefore tracks rIds per scope (:document for document-level rels, :package for package-level rels); each scope has its own registry, counter and uniqueness domain.

Constant Summary collapse

REL_TYPE_BASE =

Relationship type namespace base (also the r: namespace URI). Individual rel type constants derive from Ooxml::PartRegistry, the single source of truth for part metadata.

Ooxml::PartRegistry::OFFICE_REL_BASE
IMAGE_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:image).rel_type
HEADER_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:header).rel_type
Ooxml::PartRegistry.find_by_key(:footer).rel_type
Ooxml::PartRegistry.find_by_key(:hyperlink).rel_type
CHART_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:chart).rel_type
FOOTNOTES_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:footnotes).rel_type
ENDNOTES_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:endnotes).rel_type
THEME_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:theme).rel_type
NUMBERING_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:numbering).rel_type

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeIdAllocator

Returns a new instance of IdAllocator.



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/uniword/docx/id_allocator.rb', line 42

def initialize
  @rid_counters = Hash.new(0) # scope -> high-water mark
  # [scope, target, type] -> { id, type, target, target_mode, scope }
  @rid_entries = {}
  @footnote_counter = 1
  @endnote_counter = 1
  @bookmark_counter = 0
  @comment_counter = 0
  @para_counter = 0
  @rsid_counter = 0
end

Class Method Details

.populate_from_package(package) ⇒ Object

Build an allocator seeded from every source on a package in one call. Used by both Package#populate_allocator and DocumentBuilder.from_template so they cannot drift apart.



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/uniword/docx/id_allocator.rb', line 191

def self.populate_from_package(package)
  alloc = new
  alloc.seed_from_rels(package.document_rels&.relationships)
  alloc.seed_from_rels(package.package_rels&.relationships,
                       scope: :package)
  alloc.seed_from_notes(
    package.footnotes&.footnote_entries,
    package.endnotes&.endnote_entries,
  )
  alloc.seed_from_comments(comment_entries_of(package))
  alloc
end

Instance Method Details

#all_rels(scope: nil) ⇒ Object

Produce the final ordered list of all allocated relationships.

Parameters:

  • scope (Symbol, nil) (defaults to: nil)

    restrict to one scope; nil for all



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

def all_rels(scope: nil)
  entries = @rid_entries.values
  entries = entries.select { |r| r[:scope] == scope } if scope
  entries.sort_by { |r| r[:id][/\d+/]&.to_i || 0 }
end

#alloc_bookmark_idObject



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

def alloc_bookmark_id
  @bookmark_counter += 1
  @bookmark_counter.to_s
end

#alloc_comment_idObject



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

def alloc_comment_id
  @comment_counter += 1
  @comment_counter.to_s
end

#alloc_endnote_idObject



112
113
114
115
116
# File 'lib/uniword/docx/id_allocator.rb', line 112

def alloc_endnote_id
  id = @endnote_counter
  @endnote_counter += 1
  id
end

#alloc_footnote_idObject



106
107
108
109
110
# File 'lib/uniword/docx/id_allocator.rb', line 106

def alloc_footnote_id
  id = @footnote_counter
  @footnote_counter += 1
  id
end

#alloc_para_idObject



128
129
130
131
# File 'lib/uniword/docx/id_allocator.rb', line 128

def alloc_para_id
  @para_counter += 1
  Digest::SHA256.hexdigest("para:#{@para_counter}").upcase[0, 8]
end

#alloc_rid(target:, type:, target_mode: nil, scope: :document) ⇒ String

Allocate a relationship ID for a target+type pair. Returns existing rId if this target+type was already registered.

Parameters:

  • target (String)

    relationship target

  • type (String)

    relationship type

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

    e.g. "External"

  • scope (Symbol) (defaults to: :document)

    :document or :package (rels part namespace)

Returns:

  • (String)

    the allocated (or existing) rId



62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/uniword/docx/id_allocator.rb', line 62

def alloc_rid(target:, type:, target_mode: nil, scope: :document)
  key = [scope, target, type.to_s]
  @rid_entries[key] ||= begin
    @rid_counters[scope] += 1
    @rid_counters[scope] += 1 while rid_id_taken?(
      "rId#{@rid_counters[scope]}", scope
    )
    { id: "rId#{@rid_counters[scope]}", type: type.to_s,
      target: target, target_mode: target_mode, scope: scope }
  end
  @rid_entries[key][:id]
end

#alloc_rsidObject



133
134
135
136
# File 'lib/uniword/docx/id_allocator.rb', line 133

def alloc_rsid
  @rsid_counter += 1
  Digest::SHA256.hexdigest("rsid:#{@rsid_counter}").upcase[0, 8]
end

#next_free_rid(scope: :document) ⇒ String

The next free rId past the high-water mark of a scope — guaranteed not registered to any target+type in that scope.

Parameters:

  • scope (Symbol) (defaults to: :document)

    :document or :package

Returns:

  • (String)

    e.g. "rId7"



100
101
102
103
104
# File 'lib/uniword/docx/id_allocator.rb', line 100

def next_free_rid(scope: :document)
  candidate = @rid_counters[scope] + 1
  candidate += 1 while rid_id_taken?("rId#{candidate}", scope)
  "rId#{candidate}"
end

#register_rid(id, target:, type:, target_mode: nil, scope: :document) ⇒ String

Register an explicit rId for a target+type pair. Used when a relationship id is assigned outside the counter (rId dedup repair) so later lookups stay consistent.

Parameters:

  • id (String)

    relationship ID to register

  • target (String)

    relationship target

  • type (String)

    relationship type

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

    e.g. "External"

  • scope (Symbol) (defaults to: :document)

    :document or :package

Returns:

  • (String)

    the registered id



85
86
87
88
89
90
91
92
93
# File 'lib/uniword/docx/id_allocator.rb', line 85

def register_rid(id, target:, type:, target_mode: nil, scope: :document)
  @rid_entries[[scope, target, type.to_s]] = {
    id: id, type: type.to_s, target: target, target_mode: target_mode,
    scope: scope
  }
  num = id[/\ArId(\d+)\z/, 1]&.to_i || 0
  @rid_counters[scope] = [@rid_counters[scope], num].max
  id
end

#rid_for(target:, type:, scope: :document) ⇒ Object

Check if a relationship has been registered for a target+type.



183
184
185
186
# File 'lib/uniword/docx/id_allocator.rb', line 183

def rid_for(target:, type:, scope: :document)
  key = [scope, target, type.to_s]
  @rid_entries[key]&.fetch(:id, nil)
end

#seed_from_comments(comments) ⇒ Object

Seed the comment counter from existing comment entries so newly allocated comment IDs do not collide with loaded ones.



167
168
169
170
171
# File 'lib/uniword/docx/id_allocator.rb', line 167

def seed_from_comments(comments)
  comments&.each do |c|
    @comment_counter = [@comment_counter, c.comment_id.to_i].max
  end
end

#seed_from_notes(footnote_entries, endnote_entries) ⇒ Object

Seed footnote/endnote counters from existing note entries.



154
155
156
157
158
159
160
161
162
163
# File 'lib/uniword/docx/id_allocator.rb', line 154

def seed_from_notes(footnote_entries, endnote_entries)
  footnote_entries&.each do |e|
    id = e.id.to_i
    @footnote_counter = [@footnote_counter, id + 1].max if id > 0
  end
  endnote_entries&.each do |e|
    id = e.id.to_i
    @endnote_counter = [@endnote_counter, id + 1].max if id > 0
  end
end

#seed_from_rels(relationships, scope: :document) ⇒ Object

Seed from a relationships collection — preserves existing rIds verbatim so a load→save round-trip is rId-stable. A loaded rId already registered to a different target+type in the same scope (only possible when allocation preceded seeding, or the source has duplicate ids) yields a fresh counter allocation for the seeded pair instead — uniqueness wins over verbatim preservation.

Parameters:

  • relationships (Array, nil)

    rels to seed

  • scope (Symbol) (defaults to: :document)

    :document or :package (rels part namespace)



147
148
149
150
151
# File 'lib/uniword/docx/id_allocator.rb', line 147

def seed_from_rels(relationships, scope: :document)
  return unless relationships

  relationships.each { |r| seed_rel(r, scope) }
end