Class: Uniword::Docx::IdAllocator
- Inherits:
-
Object
- Object
- Uniword::Docx::IdAllocator
- 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
- FOOTER_REL_TYPE =
Ooxml::PartRegistry.find_by_key(:footer).rel_type
- HYPERLINK_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
-
.populate_from_package(package) ⇒ Object
Build an allocator seeded from every source on a package in one call.
Instance Method Summary collapse
-
#all_rels(scope: nil) ⇒ Object
Produce the final ordered list of all allocated relationships.
- #alloc_bookmark_id ⇒ Object
- #alloc_comment_id ⇒ Object
- #alloc_endnote_id ⇒ Object
- #alloc_footnote_id ⇒ Object
- #alloc_para_id ⇒ Object
-
#alloc_rid(target:, type:, target_mode: nil, scope: :document) ⇒ String
Allocate a relationship ID for a target+type pair.
- #alloc_rsid ⇒ Object
-
#initialize ⇒ IdAllocator
constructor
A new instance of IdAllocator.
-
#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.
-
#register_rid(id, target:, type:, target_mode: nil, scope: :document) ⇒ String
Register an explicit rId for a target+type pair.
-
#rid_for(target:, type:, scope: :document) ⇒ Object
Check if a relationship has been registered for a target+type.
-
#seed_from_comments(comments) ⇒ Object
Seed the comment counter from existing comment entries so newly allocated comment IDs do not collide with loaded ones.
-
#seed_from_notes(footnote_entries, endnote_entries) ⇒ Object
Seed footnote/endnote counters from existing note entries.
-
#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.
Constructor Details
#initialize ⇒ IdAllocator
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.
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_id ⇒ Object
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_id ⇒ Object
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_id ⇒ Object
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_id ⇒ Object
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_id ⇒ Object
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.
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_rsid ⇒ Object
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.
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.
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.
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 |