Class: Asciidoctor::PDF::Rhrev::Converter

Inherits:
Object
  • Object
show all
Includes:
ChangeBars, Exporter, Renderer, Rhrev::Helpers
Defined in:
lib/asciidoctor/rhrev/converter.rb

Constant Summary collapse

ROLLUP_CONTEXTS =

Contexts that never get a revision-history-table row of their own: a marked one rolls up as a bullet into its nearest enclosing section's entry instead (creating that entry if the section has none yet for the matching revision), rather than needing its own id, its own destination, or a generated label. See rollup_child_revision_entries.

[:paragraph, :ulist, :olist, :dlist, :admonition, :open, :quote, :verse, :sidebar].freeze

Constants included from ChangeBars

Asciidoctor::PDF::Rhrev::ChangeBars::ARRANGED_BLOCK_CONTEXTS

Instance Method Summary collapse

Methods included from ChangeBars

#arrange_block, #bracket_change_bar, #change_bar?, #change_bar_settings, #change_bar_x, #enter_change_bar_section, #exit_change_bar_section, #init_change_bars, #ink_change_bar, #ink_change_bar_for_extent, #ink_chapter_title, #ink_general_heading, #ink_part_title, #record_change_bar_start, #take_change_bar_start

Methods included from Exporter

#build_export_description_xrefs, #convert_anchor_to_xref_for_export, #export_change_text, #export_change_with_bullets, #export_to_adoc_file, #should_export_to_file?

Methods included from Renderer

#add_custom_first_row_to_markup, #allocate_revision_history_extent, #build_cell, #build_consolidated_list, #build_description_xrefs, #build_list_row, #build_location_text, #build_table_via_parsing, #build_text_cell, #build_text_row, #build_xref_text, #check_if_numbered, #create_revision_table_properly, #format_as_list, #format_location_for_display, #format_with_role, #get_column_widths, #ink_prose, #ink_revision_history, #ink_revision_history_content, #is_initial_release?, #render_adoc_include, #render_initial_release_table, #resolve_pagerhrefs_in_table, #stamp_foreground_image

Methods included from Rhrev::Helpers

#antora_build?, #convert_anchor_to_xref, #debug_log, #format_prev_rev, #needs_asciidoc_cell?, #preprocess_attribute_content, #with_attribute_missing_suppressed

Constructor Details

#initialize(*args) ⇒ Converter

Returns a new instance of Converter.



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/asciidoctor/rhrev/converter.rb', line 25

def initialize *args
  super
  @revision_history_extent = nil
  @revision_prefix = nil
  @rhrev_deferred_pages = nil
  @pagerhref_tables = []
  @export_completed = false
  @catalog = nil
  @manual_mode = false
  @rhrev_body_start_page = nil
  @rhrev_table_cell_bar_rows = nil
end

Instance Method Details

#allocate_pagerhref_table_extent(node) ⇒ Object



760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# File 'lib/asciidoctor/rhrev/converter.rb', line 760

def allocate_pagerhref_table_extent node
  @rendering_deferred_table = true
  
  extent = dry_run onto: self do
    super_convert_table node
  end
  
  @rendering_deferred_table = false
  
  # Move cursor to after the allocated space
  extent.each_page { |first_page| start_new_page unless first_page }
  move_cursor_to extent.to.cursor
  
  # Store for later re-rendering with actual page numbers
  @pagerhref_tables << {
    node: node,
    extent: extent
  }
end

#append_child_change_to_all(revision, change_text) ⇒ Object

Appends onto the document's existing -all change text for this revision, or sets it fresh if none exists yet. Catalog#add_all_entry overwrites unconditionally, so the existing value has to be read and folded in here, unlike add_entry it has no anchor to guard against being called more than once.



556
557
558
559
560
# File 'lib/asciidoctor/rhrev/converter.rb', line 556

def append_child_change_to_all revision, change_text
  existing = revision_history.all_entries[revision]
  combined = existing.to_s.empty? ? change_text : "#{existing} * #{change_text}"
  revision_history.add_all_entry revision, combined
end

#append_child_change_to_entry(revision, anchor_node, change_text) ⇒ Object

Finds anchor_node's existing entry for this revision and appends another bullet onto its :change text, or creates the entry if anchor_node has none yet. format_as_list (renderer.rb) already turns a " * "-joined string into a rendered bulleted list, including retroactively bulleting a first item that started out as plain prose, so joining is all that's needed here, no separate bulleting step. A node that is also independently marked already has its own entry, with its own change text as the first item, by the time this runs.

Generalized over anchor_node's context (section or table so far): the section-specific fields (sectnum, is_chapter, sectname) all respond_to?-guard to nil/false for a context that doesn't have them, table's own caption_number is picked up the same way update_revision_entry_metadata already does for a table marked directly, so a synthesized table entry looks the same as one from the normal collect_revision_entries path.



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/asciidoctor/rhrev/converter.rb', line 527

def append_child_change_to_entry revision, anchor_node, change_text
  entries = revision_history.entries[revision] ||= []
  entry = entries.find { |e| e[:anchor] == anchor_node.id }
  if entry
    entry[:change] = entry[:change].to_s.empty? ? change_text : "#{entry[:change]} * #{change_text}"
  else
    is_chapter = anchor_node.context == :section && anchor_node.respond_to?(:level) &&
      anchor_node.document.doctype == 'book' && (anchor_node.level == 0 || anchor_node.level == 1)
    caption_number = nil
    if anchor_node.respond_to?(:caption) && anchor_node.caption && anchor_node.caption =~ /(\d+)/
      caption_number = $1
    end
    revision_history.add_entry revision, anchor_node.id, change_text,
      reftext: (anchor_node.respond_to?(:reftext) ? anchor_node.reftext : nil),
      title: anchor_node.title,
      sectnum: (anchor_node.respond_to?(:sectnum) ? anchor_node.sectnum : nil),
      context: anchor_node.context,
      is_chapter: is_chapter,
      sectname: (anchor_node.respond_to?(:sectname) ? anchor_node.sectname : nil),
      caption_number: caption_number,
      source_line: anchor_node.lineno
  end
end

#catalog_block_anchor(node) ⇒ Object



703
704
705
706
707
708
709
710
711
712
713
714
715
# File 'lib/asciidoctor/rhrev/converter.rb', line 703

def catalog_block_anchor node
  @anchor_catalog ||= {}
  if node.id
    @anchor_catalog[node.id] ||= {
      title: node.title,
      context: node.context
    }
    page_num = page_number
    @anchor_catalog[node.id][:dest] = { page: (rhrev_display_page page_num), y: cursor }

    revision_history.link_dest_to_page node.id, page_num, (@rhrev_body_start_page || 1), y: cursor
  end
end

#collect_document_level_entries(doc) ⇒ Object



805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
# File 'lib/asciidoctor/rhrev/converter.rb', line 805

def collect_document_level_entries doc
  return if @document_entries_collected
  @document_entries_collected = true

  # Discover revisions from *-prevrev attributes so any number of
  # segments works (1-1, 1-2-0, ...), not just major-minor
  doc.attributes.each_key do |key|
    key_str = key.to_s
    next unless key_str.end_with?('-prevrev')
    revision = key_str.delete_suffix('-prevrev')
    next unless revision.match?(/\A\d+(?:-\d+)*\z/)

    all_attr_name = "#{@revision_prefix}#{revision}-all"
    if (all_value = doc.attr(all_attr_name))
      revision_history.add_all_entry revision, all_value
    end

    cover_attr_name = "#{@revision_prefix}#{revision}-cover"
    if (cover_value = doc.attr(cover_attr_name))
      revision_history.add_cover_entry revision, cover_value
    end
  end
end

#collect_revision_entries(node) ⇒ Object



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/asciidoctor/rhrev/converter.rb', line 372

def collect_revision_entries node
  return if @prescan_complete
  return unless node.respond_to?(:attributes)
  
  is_document = node.class.to_s.include?('Document')
  return if is_document
  
  # Access raw attributes safely
  attr_entries = node.instance_variable_get(:@attributes) rescue {}
  return if attr_entries.nil? || attr_entries.empty?
  
  # Check for rhrev attributes before checking ID to debug missing IDs
  has_rhrev = attr_entries.keys.any? { |k| k.to_s.start_with?(@revision_prefix) }
  return unless has_rhrev
  
  has_id = node.id && !node.id.empty? rescue false
  unless has_id
    debug_log "Node #{node.context} has rhrev attributes but NO ID. Skipping. Attributes: #{attr_entries.keys.select{|k| k.to_s.start_with?(@revision_prefix)}}", @document
    return
  end
  
  debug_log "Found entry candidate on #{node.context} id=#{node.id}", @document
  
  attr_entries.each do |key, value|
    key_str = key.to_s
    next unless key_str.start_with?(@revision_prefix)
    next if key_str.include?('-all') || key_str.end_with?('-cover')
    
    revision = key_str.sub("#{@revision_prefix}", '')
    
    debug_log "Adding entry: Rev=#{revision}, Anchor=#{node.id}, Change=#{value}", @document
    
    revision_history.add_entry revision, node.id, value.to_s,
      reftext: nil,
      title: nil,
      sectnum: nil,
      context: node.context,
      is_chapter: false,
      sectname: nil,
      caption_number: nil,
      source_line: node.lineno
  end
end

#convert_dlist(node) ⇒ Object



312
313
314
# File 'lib/asciidoctor/rhrev/converter.rb', line 312

def convert_dlist node
  bracket_change_bar(node) { super }
end

#convert_document(node) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/asciidoctor/rhrev/converter.rb', line 74

def convert_document node
  @revision_prefix = node.attr 'revhistoryprefix', 'rhrev'
  init_change_bars node

  # Check if we're in manual mode (pagerhref support needed)
  @manual_mode = node.attr('rhrev') == 'manual'
  
  # Initialize catalog
  @catalog = revision_history
  
  # Prescan document to populate catalog before rendering
  # This is necessary for accurate space allocation
  prescan_document node
  
  # Render document (this will visit all nodes and update entry metadata)
  result = super
  
  # Ink revision history if allocated
  if @revision_history_extent && @catalog
    ink_revision_history node, @revision_history_extent
  end
  
  # Re-render tables with pagerhrefs (only in manual mode)
  ink_pagerhref_tables if @manual_mode && @pagerhref_tables && !@pagerhref_tables.empty?
  
  # Export to file AFTER rendering (metadata like sectnum, caption_number are now populated)
  if node.attr?('rhrev-export-to-file') && @catalog
    export_to_adoc_file node
  end
  
  result
end

#convert_example(node) ⇒ Object



248
249
250
251
252
253
254
255
# File 'lib/asciidoctor/rhrev/converter.rb', line 248

def convert_example node
  # Skip collect during render if prescan already did it
  collect_revision_entries node unless @prescan_complete
   node
  result = super
  catalog_block_anchor node
  result
end

#convert_floating_title(node) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
# File 'lib/asciidoctor/rhrev/converter.rb', line 266

def convert_floating_title node
  # Skip collect during render if prescan already did it
  collect_revision_entries node unless @prescan_complete
   node
  result = super
  catalog_block_anchor node
  if @change_bar_attr && !scratch? && (start_pos = take_change_bar_start node)
    ink_change_bar start_pos, { page: page_number, cursor: cursor }
  end
  result
end

#convert_image(node) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/asciidoctor/rhrev/converter.rb', line 228

def convert_image node
  # Skip collect during render if prescan already did it
  collect_revision_entries node unless @prescan_complete
   node
  # PDF targets import whole pages; there is no cursor flow to mark
  if @change_bar_attr && !scratch? && (change_bar? node) &&
      !((node.attr 'target').to_s.downcase.end_with? '.pdf')
    bar_from = { page: page_number, cursor: cursor }
  end
  result = super
  catalog_block_anchor node
  if bar_from
    bar_to = { page: page_number, cursor: cursor }
    # Images never split; a page change means the image moved wholesale
    bar_from = { page: bar_to[:page], cursor: bounds.top } if bar_to[:page] > bar_from[:page]
    ink_change_bar bar_from, bar_to
  end
  result
end

#convert_inline_quoted(node) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/asciidoctor/rhrev/converter.rb', line 335

def convert_inline_quoted node
  if node.text&.match?(/^\[pagerhref:/)
    if node.text =~ /^\[pagerhref:(.+)\]$/
      anchor = $1
      lookup_anchor = anchor.gsub('----', ':::')
      
      if @anchor_catalog && @anchor_catalog[lookup_anchor] && @anchor_catalog[lookup_anchor][:dest]
        page_num = @anchor_catalog[lookup_anchor][:dest][:page]
        %(<a anchor="#{lookup_anchor}"><span class="pagerhref">#{page_num}</span></a>)
      else
        if scratch?
          %(<a anchor="#{lookup_anchor}"><span class="pagerhref">99</span></a>)
        else
          %(<a anchor="#{lookup_anchor}"><span class="pagerhref">??</span></a>)
        end
      end
    else
      super
    end
  else
    super
  end
end

#convert_listing(node) ⇒ Object



257
258
259
260
261
262
263
264
# File 'lib/asciidoctor/rhrev/converter.rb', line 257

def convert_listing node
  # Skip collect during render if prescan already did it
  collect_revision_entries node unless @prescan_complete
   node
  result = super
  catalog_block_anchor node
  result
end

#convert_olist(node) ⇒ Object



308
309
310
# File 'lib/asciidoctor/rhrev/converter.rb', line 308

def convert_olist node
  bracket_change_bar(node) { super }
end

#convert_open(node) ⇒ Object



300
301
302
# File 'lib/asciidoctor/rhrev/converter.rb', line 300

def convert_open node
  bracket_change_bar(node) { super }
end

#convert_paragraph(node) ⇒ Object

Paragraphs, the three list types, and open blocks don't reliably route through arrange_block, unlike admonition/quote/verse/sidebar (widened directly into the existing arrange_block hook in change_bars.rb), so they need the same manual page-cursor bracket convert_table and convert_image use, factored out as bracket_change_bar since none of the five need any special-casing beyond that. open specifically: asciidoctor-pdf's convert_open only calls arrange_block when the block has a title, an id, or the unbreakable option, a plain open block skips it entirely, so bracketing the whole call here covers both paths uniformly instead of depending on which one super happens to take.

None of the nine call the collect/update/catalog trio the way section/example/table/image/floating_title do: a marked paragraph, list, admonition, open, quote, verse, or sidebar doesn't get its own revision-history-table row or its own id requirement, it rolls up as a bullet into its enclosing section's entry instead, handled entirely at prescan time by rollup_child_revision_entries.



296
297
298
# File 'lib/asciidoctor/rhrev/converter.rb', line 296

def convert_paragraph node
  bracket_change_bar(node) { super }
end

#convert_preamble(node) ⇒ Object



107
108
109
110
# File 'lib/asciidoctor/rhrev/converter.rb', line 107

def convert_preamble node
  record_rhrev_body_start_page
  super
end

#convert_rhrev(node) ⇒ Object



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/asciidoctor/rhrev/converter.rb', line 316

def convert_rhrev node
  # The HtmlTreeprocessor (misleadingly named; it has no backend guard,
  # unlike ChangeBarsHtmlTreeprocessor) unshifts a synthetic :rhrev
  # block to the very front of the document whenever :rhrev: is set to
  # anything but manual/macro, so this is normally the first body
  # block dispatched, ahead of the real preamble or first section.
  # Capture unconditionally, before the early returns: even when this
  # particular call is a no-op, the dispatch itself still marks where
  # body content actually starts.
  record_rhrev_body_start_page

  return unless node.document.attr? 'rhrev'
  return if @revision_history_extent

  collect_document_level_entries node.document
  @revision_history_extent = allocate_revision_history_extent node.document
  nil
end

#convert_section(node, opts = {}) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/asciidoctor/rhrev/converter.rb', line 112

def convert_section node, opts = {}
  # Whichever of convert_preamble/convert_section runs first for the
  # whole document is body content's own first page; a no-op on every
  # later call, including nested sections, via the memoized ivar.
  record_rhrev_body_start_page

  # Skip collect during render if prescan already did it
  collect_revision_entries node unless @prescan_complete
   node

  # Entering a section closes out any still-open ancestor change bars,
  # so a marked section's bar never bleeds into its child sections.
  # Ancestors carrying the recursive option are exempt; their bar is
  # inked below, after super has rendered every nested child.
  enter_change_bar_section node
  result = super
  exit_change_bar_section node

  if node.id
    # Read the page from pdf-page-start, set by upstream's convert_section
    # inside the super call just above, right after any chapter-opening
    # page break. A pre-super page_number would be the page before that
    # break, one page too low for any chapter-starting section.
    page_num = ((node.attr 'pdf-page-start') || page_number).to_i
    @anchor_catalog ||= {}
    @anchor_catalog[node.id] ||= {
      title: node.title,
      reftext: node.reftext,
      sectnum: node.sectnum,
      context: node.context,
      level: node.level
    }
    @anchor_catalog[node.id][:dest] = { page: (rhrev_display_page page_num), y: cursor }

    # Link destination to page for revision history
    revision_history.link_dest_to_page node.id, page_num, (@rhrev_body_start_page || 1), y: cursor
  end

  # Start position was captured at heading-ink time (after any page advance)
  if @change_bar_attr && !scratch? && (start_pos = take_change_bar_start node)
    ink_change_bar start_pos, { page: page_number, cursor: cursor }
  end
  result
end

#convert_table(node) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/asciidoctor/rhrev/converter.rb', line 179

def convert_table node
  has_id = node.id && !node.id.empty? rescue false
  has_rhrev = false
  if node.respond_to?(:attributes) && node.attributes
    has_rhrev = node.attributes.keys.any? { |k| k.to_s.start_with?(@revision_prefix || 'rhrev') } rescue false
  end

  # Check for pagerhref macros in table (only in manual mode)
  # This check is expensive so we skip it unless in manual mode
  has_pagerhref = @manual_mode && !@rendering_deferred_table && table_contains_pagerhref?(node)

  # A cell carrying the current revision's attribute gets its own
  # margin bar at draw time (table_cell_change_bar_patch.rb reads this
  # ivar back via Prawn::Table::Cell#row), independent of whether the
  # table itself is marked. See marked_table_cell_bar_rows for why
  # this matches by row index alone. Known gap: for a manual-mode
  # pagerhref table, this ivar is stale by the time
  # ink_pagerhref_tables performs the deferred real ink pass later,
  # so cell-level bars do not reach that combination.
  @rhrev_table_cell_bar_rows = marked_table_cell_bar_rows(node) if @change_bar_attr && !scratch?

  if !has_id && !has_rhrev && !has_pagerhref
    return super
  end

  # Skip collect during render if prescan already did it
  collect_revision_entries node unless @prescan_complete
   node

  if has_pagerhref
    allocate_pagerhref_table_extent node
  else
    # Skip recording when the upstream table-container rewrap will trigger
    # (condition replicated from asciidoctor-pdf convert_table); the
    # re-entrant conversion of the attribute-preserving dup records instead
    if @change_bar_attr && !scratch? && (change_bar? node) &&
        !(!at_page_top? && ((node.option? 'unbreakable') ||
          ((node.option? 'breakable') && (node.id || node.title?))))
      bar_from = { page: page_number, cursor: cursor }
    end
    result = super
    catalog_block_anchor node
    ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
    result
  end
ensure
  @rhrev_table_cell_bar_rows = nil
end

#convert_ulist(node) ⇒ Object



304
305
306
# File 'lib/asciidoctor/rhrev/converter.rb', line 304

def convert_ulist node
  bracket_change_bar(node) { super }
end

#enclosing_section(node) ⇒ Object

Walks up from node to its nearest enclosing :section, or nil if none exists, i.e. node sits in the document preamble. Shared by rollup_child_revision_entries and preamble_rollup_node? below.



438
439
440
441
442
# File 'lib/asciidoctor/rhrev/converter.rb', line 438

def enclosing_section node
  section = node.parent
  section = section.parent until section.nil? || section.context == :section
  section
end

#ink_pagerhref_tablesObject



785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
# File 'lib/asciidoctor/rhrev/converter.rb', line 785

def ink_pagerhref_tables
  return if scratch?
  
  @anchor_catalog ||= {}
  
  @pagerhref_tables.each do |table_info|
    extent = table_info[:extent]
    node = table_info[:node]
    
    # Go back to the allocated space
    go_to_page extent.from.page
    move_cursor_to extent.from.cursor
    
    # Re-render the table with flag set to prevent recursion
    @rendering_deferred_table = true
    super_convert_table node
    @rendering_deferred_table = false
  end
end

#marked_table_cell_bar_rows(node) ⇒ Object

The 0-indexed row positions (head rows then body rows, one combined sequence, matching Prawn::Table::Cell#row) that contain at least one cell carrying the current revision's change-bar attribute, or nil if none do. Row index only, not (row, column): the bar only cares which row, see convert_table for the full reasoning (colspan makes a reliable column index unsafe to compute, source_ location, tried first, turned out to always be nil under normal CLI usage and matched every cell instead of just the marked one). Extracted as its own method specifically so it can be tested directly: that bug was invisible to a real-render/pdftotext check, the table's own revision-history entry was already correct either way, only the bar geometry was wrong, and bars are graphical.



169
170
171
172
173
174
175
176
177
# File 'lib/asciidoctor/rhrev/converter.rb', line 169

def marked_table_cell_bar_rows node
  marked_rows = []
  (node.rows[:head] + node.rows[:body]).each_with_index do |row, row_idx|
    marked_rows << row_idx if row.any? do |cell|
      cell.respond_to?(:attributes) && cell.attributes && (cell.attributes.key? @change_bar_attr)
    end
  end
  marked_rows.empty? ? nil : marked_rows
end

#node_carries_own_rhrev?(node) ⇒ Boolean

True when the node carries its own rhrev* attribute in its raw (non-inherited) attribute set -- the same check collect_revision_entries makes, hoisted out so prescan_document can gate update_revision_entry_metadata on it.

Returns:

  • (Boolean)


419
420
421
422
423
424
425
# File 'lib/asciidoctor/rhrev/converter.rb', line 419

def node_carries_own_rhrev? node
  return false unless node.respond_to?(:attributes)
  raw = (node.instance_variable_get(:@attributes) rescue nil)
  return false if raw.nil? || raw.empty?
  prefix = @revision_prefix || 'rhrev'
  raw.keys.any? { |k| k.to_s.start_with?(prefix) }
end

#preamble_rollup_node?(node) ⇒ Boolean

True when node is one of the rollup contexts and has no valid section to roll up into: no enclosing section (the document preamble), or one with no id to anchor an entry on. rollup_child_revision_entries uses this to route such a node's change text to the document-level -all entry instead of a section entry, since there's no section anchor available either way.

Returns:

  • (Boolean)


450
451
452
453
# File 'lib/asciidoctor/rhrev/converter.rb', line 450

def preamble_rollup_node? node
  return false unless ROLLUP_CONTEXTS.include? node.context
  (section = enclosing_section node).nil? || section.id.nil?
end

#prescan_document(doc) ⇒ Object



829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'lib/asciidoctor/rhrev/converter.rb', line 829

def prescan_document doc
  debug_log "Starting prescan_document", doc
  
  # Suppress attribute missing warnings during prescan
  # This prevents counter attributes like {counter:tablecounter} from incrementing
  with_attribute_missing_suppressed doc do
    collect_document_level_entries doc
    
    # Use find_by with context filter - more efficient than single traversal
    # because find_by(context:) can skip non-matching subtrees
    [:section, :floating_title, :example, :listing, :table, :image].each do |ctx|
      doc.find_by(context: ctx).each do |node|
        collect_revision_entries node
        # Populate the *real* title/sectnum/reftext/caption_number now, from
        # the parsed AST, so the extent dry-run measures the same content the
        # deferred backfill will later ink. Gated on the node carrying its own
        # rhrevN-M attribute: reusing node.attributes here (inherited config keys
        # like rhrev-table-caption match the prefix) would turn this into a full
        # per-node document walk.
         node if node_carries_own_rhrev? node
      end
    end

    propagate_cell_content_marks doc
    rollup_child_revision_entries doc
    rollup_table_cell_revision_entries doc

    @prescan_complete = true
    debug_log "Prescan complete. Catalog has #{revision_history.entries.values.flatten.size} entries.", doc
  end
end

#propagate_cell_content_marks(doc) ⇒ Object

A table cell marked with an rhrev attribute is not a rollup context (ROLLUP_CONTEXTS): a cell rolls up to its own enclosing table, not to the nearest section, and table cells are not reachable through the same context-based find_by a normal block-level node is, so this walks node.rows/[:body] directly instead. Otherwise the same three-tier fallback as rollup_child_revision_entries: append onto the table's own entry (creating it, with full metadata, if the table carries no rhrev attribute of its own) when the table has an id to anchor on; otherwise the table's nearest enclosing section's entry, the same fallback a marked paragraph or list already uses; only when neither exists (the table sits in the document preamble, no section at all) does this fall back to the document-level -all entry. -all is for document-wide, structural change text, not a substitute anchor for "this table happened not to get an id": a table with no id of its own but sitting inside a real section almost always has one, and a single cell's change does not belong at the document level just because nobody bothered with an id.

Must run at prescan time, same reason as rollup_child_revision_entries: allocate_revision_history_extent measures space for the revision- history table before any user table has actually rendered, so a cell-driven entry has to exist before that measurement, not merely by the time the table itself renders. A plain table cell's content is never parsed as blocks, so an attribute list inside one is just literal text, and a Table::Cell's own attributes are populated only by the fixed cell-specifier grammar (colspan, rowspan, style, halign, valign), never by a general attribute list. An AsciiDoc-style (a|) cell's content does parse as blocks, so [rhrev1-2="..."] before content inside one parses and attaches the normal way, just to that nested block, not to the cell. That nested block is not reachable from the outer document at all (its inner_document is a separate tree, confirmed: find_by on the outer document returns zero results for it), so rollup_table_cell_revision_entries and the draw-time bar patch, which both read the cell's own attributes, would never see it.

Copies any rhrev-prefixed attribute from a marked nested block onto the enclosing cell instead, then strips it from the nested block. The strip matters, not just tidiness: asciidoctor-pdf renders an a| cell's content by calling pdf.traverse on it directly (lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb), through the same converter, so a paragraph left marked would also independently trigger convert_paragraph's own bar-inking hook during that nested traversal, with cursor coordinates from inside the cell's bounded box, not a reliable page-margin position, alongside whatever the cell-level mechanism already draws correctly. Stripping it after copying leaves exactly one bar, drawn with real, resolved geometry.

Must run before rollup_table_cell_revision_entries and before real rendering, so the cell's own attributes are already in place by the time either reads them.



614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/asciidoctor/rhrev/converter.rb', line 614

def propagate_cell_content_marks doc
  doc.find_by(context: :table).each do |table|
    cells = (table.rows[:head] + table.rows[:body]).flatten
    cells.each do |cell|
      next unless cell.style == :asciidoc
      inner = (cell.inner_document rescue nil)
      next unless inner

      inner.find_by { |node| ROLLUP_CONTEXTS.include? node.context }.each do |node|
        attr_entries = (node.instance_variable_get(:@attributes) rescue nil)
        next if attr_entries.nil? || attr_entries.empty?

        attr_entries.keys.each do |key|
          key_str = key.to_s
          next unless key_str.start_with?(@revision_prefix)
          cell.set_attr key_str, attr_entries[key]
          node.remove_attr key_str
        end
      end
    end
  end
end

#record_rhrev_body_start_pageObject

The physical PDF page where the document's own body content starts rendering, preamble if it has one, otherwise the first top-level section, captured before that content's own page advances (a chapter-opening section's own break included). Mirrors the point where asciidoctor-pdf itself samples body_start_page_number, right before traverse renders anything (asciidoctor-pdf converter.rb, the body_offset = (body_start_page_number = page_number) - 1 line). Captured once, from whichever of convert_preamble/convert_section runs first; every later call is a no-op via the memoized ivar. Used as the start_page_number fed to Catalog#link_dest_to_page, so entries display the reader-visible page (matching page_numbering_ start_at: body, the default), not a raw physical index that runs ahead by however many roman-numeral-numbered front-matter pages a title page, TOC, or preamble content added before the body starts. Other page_numbering_start_at modes (toc, after-toc, cover, an explicit integer) are not accounted for and stay a known limitation.



58
59
60
# File 'lib/asciidoctor/rhrev/converter.rb', line 58

def record_rhrev_body_start_page
  @rhrev_body_start_page ||= page_number unless scratch?
end

#revision_historyObject



38
39
40
# File 'lib/asciidoctor/rhrev/converter.rb', line 38

def revision_history
  @catalog ||= Catalog.new
end

#rhrev_display_page(physical_page_number) ⇒ Object

Converts a physical PDF page number to the reader-visible page number: a plain page number once body numbering has started, a lowercase roman numeral for anything still in the front matter. Mirrors Catalog#link_dest_to_page's own conversion, kept in sync so revision-history entries and pagerhref: display the same number for the same physical page.



68
69
70
71
72
# File 'lib/asciidoctor/rhrev/converter.rb', line 68

def rhrev_display_page physical_page_number
  start_at = @rhrev_body_start_page || 1
  virtual = physical_page_number - (start_at - 1)
  (virtual < 1 ? (RomanNumeral.new physical_page_number, :lower) : virtual).to_s
end

#rollup_child_revision_entries(doc) ⇒ Object

Gathers every marked node across all nine rollup contexts in one unified traversal (a raw find_by predicate, not the per-context-type find_by(context:) loop above), specifically so they come back in true document order. node.lineno is not a usable substitute for sorting afterward: it is nil unless the document was parsed with sourcemap: true, which asciidoctor-pdf's CLI does not enable, so a sort keyed on it silently no-ops and leaves nodes grouped by whichever context find_by(context:) happened to visit first. Get a single predicate-based find_by to do the ordering instead of fixing it after the fact.

True document order matters here because the per-context-type loop above finds every paragraph in the whole document before it finds any list, regardless of which one actually comes first inside a given section, which would scramble bullet order within a section's entry. This pass must still run after that loop, so a section's own entry, if it independently carries an rhrev attribute, already exists for a child's bullet to find and append onto instead of racing to create a duplicate.

Content with no enclosing section, or one with no id (the document preamble, in practice), has no section anchor to roll up into, so its change text goes to the document-level -all entry instead: appended onto one the document already sets via its own rhrevN-M-all attribute, or creating one fresh if it doesn't. The -all row already renders without a page number (the localized "All" label takes that column instead) and already runs through the same format_as_list bulleting as a section entry, so nothing new is needed on the rendering side, only on which entry this appends to.



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/asciidoctor/rhrev/converter.rb', line 484

def rollup_child_revision_entries doc
  nodes = doc.find_by { |node| ROLLUP_CONTEXTS.include? node.context }
  nodes.select! { |node| node_carries_own_rhrev? node }

  nodes.each do |node|
    attr_entries = (node.instance_variable_get(:@attributes) rescue nil)
    next if attr_entries.nil? || attr_entries.empty?

    section = enclosing_section node
    has_section = section && section.id

    attr_entries.each do |key, value|
      key_str = key.to_s
      next unless key_str.start_with?(@revision_prefix)
      next if key_str.include?('-all') || key_str.end_with?('-cover')

      revision = key_str.sub("#{@revision_prefix}", '')
      if has_section
        append_child_change_to_entry revision, section, value.to_s
      else
        append_child_change_to_all revision, value.to_s
      end
    end
  end
end

#rollup_table_cell_revision_entries(doc) ⇒ Object



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# File 'lib/asciidoctor/rhrev/converter.rb', line 637

def rollup_table_cell_revision_entries doc
  doc.find_by(context: :table).each do |table|
    cells = (table.rows[:head] + table.rows[:body]).flatten
    cells.each do |cell|
      attr_entries = (cell.instance_variable_get(:@attributes) rescue nil)
      next if attr_entries.nil? || attr_entries.empty?

      attr_entries.each do |key, value|
        key_str = key.to_s
        next unless key_str.start_with?(@revision_prefix)
        next if key_str.include?('-all') || key_str.end_with?('-cover')

        revision = key_str.sub("#{@revision_prefix}", '')
        if table.id
          append_child_change_to_entry revision, table, value.to_s
        elsif (section = enclosing_section(table)) && section.id
          append_child_change_to_entry revision, section, value.to_s
        else
          append_child_change_to_all revision, value.to_s
        end
      end
    end
  end
end

#start_new_chapter(chapter) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/asciidoctor/rhrev/converter.rb', line 359

def start_new_chapter chapter
  if !@revision_history_extent && chapter.document && (chapter.document.attr? 'rhrev')
    rhrev_value = chapter.document.attr('rhrev')
    export_to_file = chapter.document.attr? 'rhrev-export-to-file'
    # Effectively only when rhrev is 'true', add the revision history section at the beginning of the document (after the title_page)
    if rhrev_value != 'macro' && rhrev_value != 'manual' && !export_to_file
      collect_document_level_entries chapter.document
      @revision_history_extent = allocate_revision_history_extent chapter.document
    end
  end
  super
end

#super_convert_table(node) ⇒ Object



780
781
782
783
# File 'lib/asciidoctor/rhrev/converter.rb', line 780

def super_convert_table node
  # Call the parent class method directly to avoid recursion
  self.class.superclass.instance_method(:convert_table).bind(self).call(node)
end

#table_contains_pagerhref?(node) ⇒ Boolean

Returns:

  • (Boolean)


717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/asciidoctor/rhrev/converter.rb', line 717

def table_contains_pagerhref? node
  return false unless node.context == :table
  
  # Check cell content WITHOUT triggering attribute substitution
  # This prevents counter attributes like {counter:tablecounter} from incrementing
  # We MUST NOT call cell.text or access cell.inner_document as those trigger parsing
  node.rows[:body].each do |row|
    row.each do |cell|
      # Check the raw @text instance variable if already evaluated
      if cell.instance_variable_defined?(:@text)
        raw_text = cell.instance_variable_get(:@text).to_s
        return true if raw_text.include?('pagerhref:')
      end
      # Check the inner document's source lines if available (before parsing)
      if cell.instance_variable_defined?(:@inner_document)
        inner_doc = cell.instance_variable_get(:@inner_document)
        if inner_doc && inner_doc.instance_variable_defined?(:@lines)
          lines = inner_doc.instance_variable_get(:@lines) || []
          return true if lines.any? { |l| l.to_s.include?('pagerhref:') }
        end
      end
      # Last resort: check cell's style attribute for AsciiDoc cells
      # AsciiDoc cells (a|) will have inner content that might contain pagerhref
      if cell.style == :asciidoc
        # For asciidoc cells, we need to check the source
        # Access the cell's source blocks if available
        if cell.instance_variable_defined?(:@inner_document)
          inner = cell.instance_variable_get(:@inner_document)
          if inner && inner.respond_to?(:blocks)
            inner.blocks.each do |block|
              if block.instance_variable_defined?(:@lines)
                lines = block.instance_variable_get(:@lines) || []
                return true if lines.any? { |l| l.to_s.include?('pagerhref:') }
              end
            end
          end
        end
      end
    end
  end
  false
end

#update_revision_entry_metadata(node) ⇒ Object



662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
# File 'lib/asciidoctor/rhrev/converter.rb', line 662

def  node
  return unless node.id
  return unless node.respond_to?(:attributes)
  
  with_attribute_missing_suppressed node.document do
    node.attributes.each do |key, value|
      key_str = key.to_s
      next unless key_str.start_with?(@revision_prefix)
      next if key_str.include?('-all') || key_str.end_with?('-cover')
      
      revision = key_str.sub("#{@revision_prefix}", '')
      
      entries = revision_history.entries[revision]
      next unless entries
      
      entry = entries.find { |e| e[:anchor] == node.id }
      next unless entry
      
      if entry[:change].to_s.empty?
        entry[:change] = value
      end
      
      entry[:reftext] = node.reftext
      entry[:title] = node.title
      entry[:sectnum] = node.respond_to?(:sectnum) ? node.sectnum : nil
      
      if node.context == :section && node.document.doctype == 'book'
        entry[:is_chapter] = (node.level == 0 || node.level == 1)
      end
      
      if node.respond_to?(:caption) && node.caption
        if node.caption =~ /(\d+)/
          entry[:caption_number] = $1
        end
      end
      
      entry[:sectname] = node.sectname if node.respond_to?(:sectname)
    end
  end
end