Module: Uniword::Docx::Reconciler::Helpers
- Included in:
- Uniword::Docx::Reconciler
- Defined in:
- lib/uniword/docx/reconciler/helpers.rb
Overview
Shared helpers used across all reconciliation modules.
Provides ID generation, document traversal, element_order manipulation, run utilities, and YAML config loading.
Constant Summary collapse
- CONFIG_DIR =
-- YAML config --
File.join(__dir__, "../../../../config")
Instance Method Summary collapse
-
#adjacent_run_indices(paragraph) ⇒ Object
Returns the set of run indices (0-based into runs) where the run at that index is immediately preceded by another run in element_order (no non-run elements between them).
- #assign_note_pr(settings, type) ⇒ Object
-
#backfill_paragraphs(paragraphs, rsid, id_seed) ⇒ Object
Shared paragraph backfill: assign rsid, paraId, textId defaults.
- #build_notes_collection(type, entries:) ⇒ Object
-
#build_rel(id, type, target, target_mode: nil) ⇒ Object
-- Relationship builders --.
- #can_merge?(prev, current) ⇒ Boolean
-
#consolidate_runs(paragraph) ⇒ Object
Merge adjacent runs with identical formatting and consolidate standalone tab/br runs into following text runs (F4 + F5).
- #consolidate_runs_in_body(body) ⇒ Object
- #document_fingerprint ⇒ Object
-
#empty_run?(run) ⇒ Boolean
-- Run utilities (delegate to RunUtils) --.
-
#ensure_element_in_order(model, tag_name, after: nil, before: nil) ⇒ Object
Idempotent singleton insert in schema position.
- #generate_hex_id(seed = 0) ⇒ Object
-
#generate_rsid ⇒ Object
-- ID generation --.
- #hex_derive(seed, byte_count) ⇒ Object
-
#insert_element_order(obj, name, position) ⇒ Object
Positional insert for repeatable elements.
- #load_font_metadata ⇒ Object
- #load_latent_styles_config ⇒ Object
- #merge_run_into(target, source) ⇒ Object
- #note_entries_for(notes, type) ⇒ Object
- #note_reference_from_run(run, type) ⇒ Object
-
#notes_collection_for(type) ⇒ Object
-- Note-type dispatch --.
-
#notes_part_for(type) ⇒ Object
Package part path of the notes collection for the note type.
- #run_properties_match?(a, b) ⇒ Boolean
-
#set_mc_ignorable(model, prefixes: EXTENSION_PREFIXES) ⇒ Object
-- mc:Ignorable helpers --.
- #set_notes_collection(notes, type) ⇒ Object
- #strip_empty_runs(paragraph) ⇒ Object
- #strip_empty_runs_from_notes(entries) ⇒ Object
- #text_only_run?(run) ⇒ Boolean
- #walk_all_paragraphs(&block) ⇒ Object
-
#walk_body_paragraphs(body) ⇒ Object
-- Traversal --.
- #walk_body_tables(body) ⇒ Object
- #walk_table_paragraphs(table) ⇒ Object
Instance Method Details
#adjacent_run_indices(paragraph) ⇒ Object
Returns the set of run indices (0-based into runs) where the run at that index is immediately preceded by another run in element_order (no non-run elements between them).
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 181 def adjacent_run_indices(paragraph) eo = paragraph.element_order return (0...paragraph.runs.size - 1).to_a unless eo && !eo.empty? indices = Set.new run_count = 0 prev_was_run = false eo.each do |entry| if entry.name == "r" if prev_was_run indices << (run_count - 1) end run_count += 1 prev_was_run = true else prev_was_run = false end end indices end |
#assign_note_pr(settings, type) ⇒ Object
308 309 310 311 312 313 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 308 def assign_note_pr(settings, type) case type when :footnote then settings.footnote_pr = Wordprocessingml::FootnotePr.new when :endnote then settings.endnote_pr = Wordprocessingml::EndnotePr.new end end |
#backfill_paragraphs(paragraphs, rsid, id_seed) ⇒ Object
Shared paragraph backfill: assign rsid, paraId, textId defaults.
145 146 147 148 149 150 151 152 153 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 145 def backfill_paragraphs(paragraphs, rsid, id_seed) paragraphs.each_with_index do |para, idx| strip_empty_runs(para) para.rsid_r ||= rsid para.rsid_r_default ||= "00000000" para.para_id ||= generate_hex_id("#{id_seed}:#{idx}") para.text_id ||= "77777777" end end |
#build_notes_collection(type, entries:) ⇒ Object
301 302 303 304 305 306 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 301 def build_notes_collection(type, entries:) case type when :footnote then Wordprocessingml::Footnotes.new(footnote_entries: entries) when :endnote then Wordprocessingml::Endnotes.new(endnote_entries: entries) end end |
#build_rel(id, type, target, target_mode: nil) ⇒ Object
-- Relationship builders --
236 237 238 239 240 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 236 def build_rel(id, type, target, target_mode: nil) attrs = { id: id, type: type, target: target } attrs[:target_mode] = target_mode if target_mode Ooxml::Relationships::Relationship.new(**attrs) end |
#can_merge?(prev, current) ⇒ Boolean
246 247 248 249 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 246 def can_merge?(prev, current) Builder::RunUtils.text_only_run?(prev) && Builder::RunUtils.text_only_run?(current) end |
#consolidate_runs(paragraph) ⇒ Object
Merge adjacent runs with identical formatting and consolidate standalone tab/br runs into following text runs (F4 + F5). Respects element_order: only merges runs that are truly adjacent in document order (not separated by hyperlinks, bookmarks, etc.).
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 159 def consolidate_runs(paragraph) runs = paragraph.runs return if runs.nil? || runs.size < 2 run_indices = adjacent_run_indices(paragraph) merged = [runs.first] runs[1..].each_with_index do |run, idx| prev = merged.last if run_indices.include?(idx) && run_properties_match?(prev, run) && can_merge?(prev, run) merge_run_into(prev, run) else merged << run end end paragraph.runs = merged end |
#consolidate_runs_in_body(body) ⇒ Object
202 203 204 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 202 def consolidate_runs_in_body(body) walk_body_paragraphs(body) { |p| consolidate_runs(p) } end |
#document_fingerprint ⇒ Object
31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 31 def document_fingerprint @document_fingerprint ||= begin body = package.document&.body return "empty" unless body texts = [] walk_body_paragraphs(body) do |para| texts << (para.runs || []).map(&:text_string).join end Digest::SHA256.hexdigest(texts.join("|")) end end |
#empty_run?(run) ⇒ Boolean
-- Run utilities (delegate to RunUtils) --
123 124 125 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 123 def empty_run?(run) Builder::RunUtils.empty_run?(run) end |
#ensure_element_in_order(model, tag_name, after: nil, before: nil) ⇒ Object
Idempotent singleton insert in schema position.
104 105 106 107 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 104 def ensure_element_in_order(model, tag_name, after: nil, before: nil) Ooxml::ElementOrder.insert_once(model, tag_name, after: after, before: before) end |
#generate_hex_id(seed = 0) ⇒ Object
21 22 23 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 21 def generate_hex_id(seed = 0) hex_derive("paraId:#{seed}", 4) end |
#generate_rsid ⇒ Object
-- ID generation --
17 18 19 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 17 def generate_rsid "00#{hex_derive("rsid", 3)}" end |
#hex_derive(seed, byte_count) ⇒ Object
25 26 27 28 29 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 25 def hex_derive(seed, byte_count) Digest::SHA256.hexdigest( "#{document_fingerprint}:#{seed}" )[0...(byte_count * 2)].upcase end |
#insert_element_order(obj, name, position) ⇒ Object
Positional insert for repeatable elements.
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 110 def insert_element_order(obj, name, position) order = obj.element_order return unless order return if order.any? { |e| e.name == name } Ooxml::ElementOrder.insert_at( obj, position, Lutaml::Xml::Element.new("Element", name, node_type: :element) ) end |
#load_font_metadata ⇒ Object
210 211 212 213 214 215 216 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 210 def path = File.join(CONFIG_DIR, "font_metadata.yml") YAML.load_file(path)["fonts"] rescue StandardError => e Uniword.logger&.warn { "Font metadata load failed: #{e.}" } nil end |
#load_latent_styles_config ⇒ Object
218 219 220 221 222 223 224 225 226 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 218 def load_latent_styles_config path = File.join(CONFIG_DIR, "latent_styles.yml") YAML.load_file(path) rescue StandardError => e Uniword.logger&.warn do "Latent styles config load failed: #{e.}" end nil end |
#merge_run_into(target, source) ⇒ Object
255 256 257 258 259 260 261 262 263 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 255 def merge_run_into(target, source) Builder::RunUtils.merge_text(target, source) target.tab ||= source.tab target.break ||= source.break target.position_tab ||= source.position_tab target.no_break_hyphen ||= source.no_break_hyphen target.del_text ||= source.del_text end |
#note_entries_for(notes, type) ⇒ Object
285 286 287 288 289 290 291 292 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 285 def note_entries_for(notes, type) return [] unless notes case type when :footnote then notes.footnote_entries when :endnote then notes.endnote_entries end end |
#note_reference_from_run(run, type) ⇒ Object
294 295 296 297 298 299 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 294 def note_reference_from_run(run, type) case type when :footnote then run.footnote_reference when :endnote then run.endnote_reference end end |
#notes_collection_for(type) ⇒ Object
-- Note-type dispatch --
Single source of truth for :footnote/:endnote discrimination. Adding a third note type means editing only these helpers.
270 271 272 273 274 275 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 270 def notes_collection_for(type) case type when :footnote then package.footnotes when :endnote then package.endnotes end end |
#notes_part_for(type) ⇒ Object
Package part path of the notes collection for the note type.
278 279 280 281 282 283 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 278 def notes_part_for(type) case type when :footnote then "word/footnotes.xml" when :endnote then "word/endnotes.xml" end end |
#run_properties_match?(a, b) ⇒ Boolean
242 243 244 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 242 def run_properties_match?(a, b) Builder::RunUtils.properties_match?(a, b) end |
#set_mc_ignorable(model, prefixes: EXTENSION_PREFIXES) ⇒ Object
-- mc:Ignorable helpers --
230 231 232 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 230 def set_mc_ignorable(model, prefixes: EXTENSION_PREFIXES) model.mc_ignorable = Ooxml::Types::McIgnorable.new(prefixes) end |
#set_notes_collection(notes, type) ⇒ Object
315 316 317 318 319 320 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 315 def set_notes_collection(notes, type) case type when :footnote then package.footnotes = notes when :endnote then package.endnotes = notes end end |
#strip_empty_runs(paragraph) ⇒ Object
127 128 129 130 131 132 133 134 135 136 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 127 def strip_empty_runs(paragraph) before = paragraph.runs.size paragraph.runs.reject! { |r| empty_run?(r) } removed = before - paragraph.runs.size return unless removed.positive? record_fix(FixCodes::EMPTY_RUNS_STRIPPED, "Stripped #{removed} empty run(s) from " \ "#{paragraph.class.name.split('::').last}") end |
#strip_empty_runs_from_notes(entries) ⇒ Object
138 139 140 141 142 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 138 def strip_empty_runs_from_notes(entries) entries.each do |entry| entry.paragraphs.each { |p| strip_empty_runs(p) } end end |
#text_only_run?(run) ⇒ Boolean
251 252 253 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 251 def text_only_run?(run) Builder::RunUtils.text_only_run?(run) end |
#walk_all_paragraphs(&block) ⇒ Object
84 85 86 87 88 89 90 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 84 def walk_all_paragraphs(&block) walk_body_paragraphs(package.document.body, &block) (package.document&. || []).each do |part| (part.content&.paragraphs || []).each(&block) end end |
#walk_body_paragraphs(body) ⇒ Object
-- Traversal --
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 46 def walk_body_paragraphs(body) if body.element_order && !body.element_order.empty? p_idx = 0 tbl_idx = 0 body.element_order.each do |entry| case entry.name when "p" yield body.paragraphs[p_idx] if body.paragraphs[p_idx] p_idx += 1 when "tbl" if body.tables[tbl_idx] walk_table_paragraphs(body.tables[tbl_idx]) { |p| yield p } end tbl_idx += 1 end end else body.paragraphs.each { |p| yield p } body.tables&.each { |tbl| walk_table_paragraphs(tbl) { |p| yield p } } end end |
#walk_body_tables(body) ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 68 def walk_body_tables(body) return unless body if body.element_order && !body.element_order.empty? tbl_idx = 0 body.element_order.each do |entry| next unless entry.name == "tbl" yield body.tables[tbl_idx] if body.tables[tbl_idx] tbl_idx += 1 end else body.tables&.each { |tbl| yield tbl } end end |
#walk_table_paragraphs(table) ⇒ Object
92 93 94 95 96 97 98 99 |
# File 'lib/uniword/docx/reconciler/helpers.rb', line 92 def walk_table_paragraphs(table) return unless table table.rows&.each do |row| row.cells&.each do |cell| cell.paragraphs.each { |p| yield p } end end end |