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

DLIST_STYLES_ROUTING_THROUGH_LIST_ITEM =

dlist's unordered/ordered/qanda styles synthesize real ListItem objects and route them through convert_list_item the same as ulist/olist, confirmed directly against asciidoctor-pdf's own source (convert_dlist's own case statement, then convert_list), so per-item targeting reaches them too. The default and horizontal styles render every term/description pair inline inside convert_dlist itself, with no per-entry call to override at all, the same structural gap table-row-level marking has; per-item targets on one of those two styles fall back to marking the whole block instead, with a warning, rather than silently doing nothing.

%w(unordered ordered qanda).freeze
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, #record_section_heading_position, #take_change_bar_start, #take_section_heading_position

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.



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

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_node = nil
end

Instance Method Details

#allocate_pagerhref_table_extent(node) ⇒ Object



995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
# File 'lib/asciidoctor/rhrev/converter.rb', line 995

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.



784
785
786
787
788
# File 'lib/asciidoctor/rhrev/converter.rb', line 784

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.



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

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



938
939
940
941
942
943
944
945
946
947
948
949
950
# File 'lib/asciidoctor/rhrev/converter.rb', line 938

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), physical_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



1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
# File 'lib/asciidoctor/rhrev/converter.rb', line 1046

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



600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
# File 'lib/asciidoctor/rhrev/converter.rb', line 600

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

#compute_rhrev_page_numbering_start_page(node) ⇒ Object

The physical PDF page that displays as reader-visible page "1", computed the same way asciidoctor-pdf itself does (converter.rb's own convert_document), reading the document's own resolved page-numbering-start-at mode, rather than assuming a body-mode restart the way record_rhrev_body_start_page's own capture always has.

Called once, from convert_document, AFTER the whole document (table of contents included) has rendered: toc/after-toc genuinely cannot be answered any earlier. Confirmed directly against asciidoctor-pdf's own source: for the standard case (an automatic, top-of-document toc, not an explicit toc::[] macro), its real page extent, and therefore the toc/after-toc offset, is already known before traverse even starts (allocate_toc's own dry run measures it up front) - but for a toc::[] macro placed inside a preamble or elsewhere, that extent is only known once traverse actually reaches and inks it, and asciidoctor-pdf's own correction for that case runs only after traverse doc returns (the num_front_matter_pages = toc_extent.to.page adjustment, gated on title_as_page && !toc_at_top). Reading @toc_extent once, after super has fully returned, gives the correct, final value either way, without this method needing to know which of the two internal paths asciidoctor-pdf actually took.

Deliberately does not handle an explicit integer page-numbering-start-at: that mode's own body page is LABELED the given integer, not necessarily displayed page "1" at all (a genuinely different formula shape, physical - anchor + label, not physical - anchor + 1), and is left as the same known limitation record_rhrev_body_start_page's own prior docblock already named, rather than risk a second, differently-wrong formula for a mode not confirmed against a real build.



108
109
110
111
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
# File 'lib/asciidoctor/rhrev/converter.rb', line 108

def compute_rhrev_page_numbering_start_page node
  body_start = @rhrev_body_start_page || 1
  case (mode = @theme.page_numbering_start_at || 'body')
  when 'cover'
    # asciidoctor-pdf itself only honors page-numbering-start-at:
    # cover when a real front cover exists (front-cover-image or the
    # theme's own cover_front_image); otherwise it falls back to
    # title, not body, silently (converter.rb's own page_numbering_
    # start_at cascade), and title's offset is 0 whenever the
    # document has a title page at all (doctype book, or the
    # title-page attribute), regardless of whether that title page
    # itself renders. body_start only survives as the real answer
    # for the narrow remaining case: no front cover AND no title
    # page either.
    front_cover_path, = resolve_background_image node.document, @theme, 'front-cover-image', theme_key: :cover_front_image
    has_title_page = node.document.doctype == 'book' || (node.document.attr? 'title-page')
    (front_cover_path || has_title_page) ? 1 : body_start
  when 'toc'
    (extent = @toc_extent) ? extent.from.page : body_start
  when 'after-toc'
    if (extent = @toc_extent)
      after_toc_page = extent.to.page
      after_toc_page += 1 if @ppbook && (recto_page? after_toc_page)
      after_toc_page + 1
    else
      body_start
    end
  else
    body_start
  end
end

#convert_dlist(node) ⇒ Object



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

def convert_dlist node
  targets = list_item_change_bar_targets node
  if (::Set === targets) && !(DLIST_STYLES_ROUTING_THROUGH_LIST_ITEM.include? node.style)
    log :warn, %(rhrev per-item list marking is not supported for a dlist using the '#{node.style || 'default'}' style; marking the whole block instead)
    targets = :all
  end
  convert_targeted_list(node, targets) { super }
end

#convert_document(node) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/asciidoctor/rhrev/converter.rb', line 160

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

  # Correct every page number recorded during traversal under the
  # body-mode-only assumption record_rhrev_body_start_page always
  # makes: the real mode-aware value is only knowable now, after
  # the whole document (table of contents included) has rendered.
  # Must run before ink_revision_history/ink_pagerhref_tables below,
  # both real consumers of the numbers this corrects.
  correct_rhrev_page_numbering node

  # 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



341
342
343
344
345
346
347
348
# File 'lib/asciidoctor/rhrev/converter.rb', line 341

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



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

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



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/asciidoctor/rhrev/converter.rb', line 321

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



563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/asciidoctor/rhrev/converter.rb', line 563

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_list_item(node, list, opts = {}) ⇒ Object

A single item within a ulist/olist (and a dlist using the unordered/ordered/qanda styles, real ListItem objects asciidoctor-pdf itself synthesizes and routes through here the same way). Reads the innermost enclosing list's own target set off the top of the stack convert_targeted_list maintains, not off list itself: for a dlist's synthesized styles, list here is a brand-new List object asciidoctor-pdf builds on the fly with no attributes of its own, never the real, marked dlist node, so reading list's own attributes directly would never see the mark. The stack is what carries the real target set down correctly regardless of which path produced this call.

list.items.index node relies on plain object identity, correct here: asciidoctor-pdf's own convert_list passes each item straight from list.items into this method, no copying.

Deliberately not bracket_change_bar: its own change_bar? node gate checks the item's OWN attributes, always false here, the mark lives on the enclosing list, never the item. That gate is exactly right for every other bracket_change_bar caller (the node being bracketed is the node that was actually marked); it would silently swallow every targeted item's bar here, caught directly by real rendering showing no bar at all despite the target set correctly including this item's index. ink_targeted_ list_item_change_bar mirrors bracket_change_bar's own shape without that gate, since membership in targets already answers the "should this bar" question on its own.



528
529
530
531
532
533
534
535
# File 'lib/asciidoctor/rhrev/converter.rb', line 528

def convert_list_item node, list, opts = {}
  targets = @list_change_bar_target_stack&.last
  if (::Set === targets) && (idx = list.items.index node) && (targets.include? idx + 1)
    ink_targeted_list_item_change_bar { super }
  else
    super
  end
end

#convert_listing(node) ⇒ Object



350
351
352
353
354
355
356
357
# File 'lib/asciidoctor/rhrev/converter.rb', line 350

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



419
420
421
# File 'lib/asciidoctor/rhrev/converter.rb', line 419

def convert_olist node
  convert_targeted_list(node) { super }
end

#convert_open(node) ⇒ Object



393
394
395
# File 'lib/asciidoctor/rhrev/converter.rb', line 393

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.



389
390
391
# File 'lib/asciidoctor/rhrev/converter.rb', line 389

def convert_paragraph node
  bracket_change_bar(node) { super }
end

#convert_preamble(node) ⇒ Object



201
202
203
204
# File 'lib/asciidoctor/rhrev/converter.rb', line 201

def convert_preamble node
  record_rhrev_body_start_page
  super
end

#convert_rhrev(node) ⇒ Object



544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
# File 'lib/asciidoctor/rhrev/converter.rb', line 544

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



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/asciidoctor/rhrev/converter.rb', line 206

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

    # y must describe the same physical location page_num does: the
    # section's own start, not wherever cursor happens to sit once
    # this whole call returns. A section's body can span several
    # pages of its own (nested content, a large table, and so on);
    # a post-super cursor read then belongs to whatever page that
    # body finished on, not page_num's page, and the two stop
    # describing the same point at all. take_section_heading_
    # position (change_bars.rb) captured cursor at the same moment
    # upstream captured pdf-page-start, right before the heading
    # itself was inked, so pairing the two here always describes
    # the section's own start consistently. Reported directly
    # against a real document: two entries sharing the same
    # page_sortable sorted in the wrong relative order, one of
    # them a page-spanning section whose old post-super y actually
    # belonged to the page after the one it was paired with.
    # Falls back to the old post-super cursor only for a section
    # with no heading ink call at all (a hidden title), no worse
    # than before for that one remaining case.
    y_pos = take_section_heading_position(node) || cursor

    @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), physical_page: page_num, y: y_pos }

    # Link destination to page for revision history
    revision_history.link_dest_to_page node.id, page_num, (@rhrev_body_start_page || 1), y: y_pos
  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



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/asciidoctor/rhrev/converter.rb', line 272

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, independent of whether the table itself
  # is marked: table_cell_change_bar_patch.rb reads this node back
  # while Prawn builds the table and flags exactly the marked cells.
  # For a manual-mode pagerhref table the real, deferred render
  # happens in ink_pagerhref_tables, which sets this ivar again from
  # its own stashed node right before that render: the ensure block
  # below has long since cleared it by then.
  @rhrev_table_cell_bar_node = 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_node = nil
end

#convert_targeted_list(node, targets = (list_item_change_bar_targets node)) ⇒ Object

Shared by convert_ulist/convert_olist/convert_dlist: computes the target set (unless the caller, convert_dlist, already resolved it), pushes it so convert_list_item can read the innermost enclosing list's own targets, and either brackets the whole list (targets == :all) or leaves bar-inking to convert_list_item entirely (targets is a real index Set, or nil, not marked at all). The stack, not a single ivar, matters for a list nested inside a marked item's own content: the same shape Takes the real conversion as a block, not a bare super call inside this method: super here would look for Converter#convert_targeted_list up the ancestor chain, which does not exist, since this method's own name differs from the three real converter entry points calling it. The block, called from inside convert_ulist/convert_olist/convert_dlist themselves, keeps super resolving to the right upstream method, the same reason bracket_change_bar takes a block rather than trying to call super itself.



464
465
466
467
468
469
470
471
472
473
474
# File 'lib/asciidoctor/rhrev/converter.rb', line 464

def convert_targeted_list node, targets = (list_item_change_bar_targets node)
  @list_change_bar_target_stack ||= []
  @list_change_bar_target_stack.push targets
  if targets == :all
    bracket_change_bar(node) { yield }
  else
    yield
  end
ensure
  @list_change_bar_target_stack.pop
end

#convert_ulist(node) ⇒ Object

A list marked with only the change-text attribute, [rhrev1-2="..."], is unchanged from every prior release: bracket_change_bar covers the whole list, one bar, one roll-up bullet.

A list marked with one or more trailing positional attributes, [rhrev1-2="...",2] or [rhrev1-2="...",2,4-6], instead targets specific items by their 1-based position: no bar over the whole list, a separate bar for just the named item(s), inked by convert_list_item below. The roll-up bullet is unaffected either way, still the list's own single entry, [rhrev1-2="..."] is read exactly as before.

A bare positional attribute cannot collide with a list's own style (ulist [square], olist numbering, and so on): confirmed directly, Asciidoctor tracks positional index by each entry's own place in the bracket, not by counting unnamed entries only, so a named rhrev1-2="..." occupying position 1 never contests the trailing bare position(s) after it.



415
416
417
# File 'lib/asciidoctor/rhrev/converter.rb', line 415

def convert_ulist node
  convert_targeted_list(node) { super }
end

#correct_rhrev_page_numbering(node) ⇒ Object

Applies compute_rhrev_page_numbering_start_page's own real, mode-aware result, correcting every page number recorded during traversal under the wrong, body-mode-only assumption. A no-op, deliberately, when the corrected value matches what record_rhrev_body_start_page already captured (page-numbering- start-at: body, the common case, and any other mode this method could not confidently resolve): no entry is touched twice for nothing.



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/asciidoctor/rhrev/converter.rb', line 148

def correct_rhrev_page_numbering node
  return unless @catalog
  corrected = compute_rhrev_page_numbering_start_page node
  return if corrected == @rhrev_body_start_page
  @rhrev_body_start_page = corrected
  @catalog.recompute_display_pages corrected
  @anchor_catalog&.each_value do |entry|
    next unless (dest = entry[:dest]) && dest[:physical_page]
    dest[:page] = rhrev_display_page dest[:physical_page]
  end
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.



666
667
668
669
670
# File 'lib/asciidoctor/rhrev/converter.rb', line 666

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

#ink_pagerhref_tablesObject



1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
# File 'lib/asciidoctor/rhrev/converter.rb', line 1020

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

    # Point table_cell_change_bar_patch.rb at THIS table for the real
    # render below: convert_table's own ensure block cleared the ivar
    # long before this loop runs.
    @rhrev_table_cell_bar_node = node if @change_bar_attr && !scratch?

    # Re-render the table with flag set to prevent recursion
    @rendering_deferred_table = true
    super_convert_table node
    @rendering_deferred_table = false
    @rhrev_table_cell_bar_node = nil
  end
end

#ink_targeted_list_item_change_barObject



537
538
539
540
541
542
# File 'lib/asciidoctor/rhrev/converter.rb', line 537

def ink_targeted_list_item_change_bar
  bar_from = { page: page_number, cursor: cursor } if @change_bar_attr && !scratch?
  result = yield
  ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
  result
end

#list_item_change_bar_targets(node) ⇒ Object

The target set for the list currently being converted: :all (bar the whole list, the only behavior before this feature existed), nil (not marked for the current revision at all), or a Set of 1-based item positions parsed from one or more trailing positional attributes, expanding an N-M token into a range. Reuses change_bar? for the marked/not-marked check, the same predicate every other bar-inking decision in this file already uses, so this only ever answers for the current revnumber.



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/asciidoctor/rhrev/converter.rb', line 484

def list_item_change_bar_targets node
  return nil unless change_bar? node
  raw = node.instance_variable_get(:@attributes)
  positional_keys = raw.keys.select {|k| ::Integer === k }.sort
  return :all if positional_keys.empty?
  targets = ::Set.new
  positional_keys.each do |key|
    token = raw[key].to_s
    if (m = /\A(\d+)-(\d+)\z/.match token)
      (m[1].to_i..m[2].to_i).each {|i| targets << i }
    elsif /\A\d+\z/.match? token
      targets << token.to_i
    end
  end
  targets.empty? ? :all : targets
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)


647
648
649
650
651
652
653
# File 'lib/asciidoctor/rhrev/converter.rb', line 647

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)


678
679
680
681
# File 'lib/asciidoctor/rhrev/converter.rb', line 678

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



1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
# File 'lib/asciidoctor/rhrev/converter.rb', line 1070

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

    ::Asciidoctor::Rhrev::CellTextMarks.lift doc, @revision_prefix
    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.

CellTextMarks.lift (cell_text_marks.rb) runs right before this: it reads the same attribute list off the cell's raw text, which is the only place it survives for a plain cell or any header-row cell, so a mark no longer needs the a| form at all. Both paths set the same attributes on the cell; this one still strips the nested block for the double-bar reason described here.



849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
# File 'lib/asciidoctor/rhrev/converter.rb', line 849

def propagate_cell_content_marks doc
  doc.find_by(context: :table).each do |table|
    cells = (table.rows[:head] + table.rows[:body] + table.rows[:foot]).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.

Correct as the FINAL value only for page-numbering-start-at: body (the default): every entry recorded against it during traversal (Catalog#link_dest_to_page, @anchor_catalog below) is provisional for any other mode, corrected once, after the whole document has rendered, by correct_rhrev_page_numbering (convert_document, see its own docblock for why the correction cannot happen any earlier for toc/after-toc specifically).



60
61
62
# File 'lib/asciidoctor/rhrev/converter.rb', line 60

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

#revision_historyObject



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

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.



70
71
72
73
74
# File 'lib/asciidoctor/rhrev/converter.rb', line 70

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.



712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
# File 'lib/asciidoctor/rhrev/converter.rb', line 712

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



872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
# File 'lib/asciidoctor/rhrev/converter.rb', line 872

def rollup_table_cell_revision_entries doc
  doc.find_by(context: :table).each do |table|
    cells = (table.rows[:head] + table.rows[:body] + table.rows[:foot]).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



587
588
589
590
591
592
593
594
595
596
597
598
# File 'lib/asciidoctor/rhrev/converter.rb', line 587

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



1015
1016
1017
1018
# File 'lib/asciidoctor/rhrev/converter.rb', line 1015

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)


952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
# File 'lib/asciidoctor/rhrev/converter.rb', line 952

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



897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
# File 'lib/asciidoctor/rhrev/converter.rb', line 897

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