Module: Asciidoctor::PDF::Rhrev::ChangeBars

Included in:
Converter
Defined in:
lib/asciidoctor/rhrev/change_bars.rb

Overview

Inks a vertical change bar in the page margin next to blocks marked with the current revision (revnumber 1.2 => attribute rhrev1-2). Enabled by the rhrev-change-bars document attribute.

Styling comes from the PDF theme by default (rhrev_change_bars category: color, width, offset, side), overridable per document via the rhrev-change-bars-color/-width/-offset/-side attributes.

Constant Summary collapse

ARRANGED_BLOCK_CONTEXTS =

Contexts that render through this hook unconditionally in asciidoctor-pdf: examples and listings (original coverage), plus admonition, quote, verse, and sidebar, so widening this one check is the whole fix for their bar-inking. open is deliberately NOT here: 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 and just traverses directly, so it needs its own manual bracket instead (see Converter#convert_open), the same shape as paragraphs and the three list types below. Tables and images never reach this hook with their own node either and are handled the same way.

[:example, :listing, :admonition, :quote, :verse, :sidebar].freeze

Instance Method Summary collapse

Instance Method Details

#arrange_block(node, &block) ⇒ Object

The extent yielded here is exact across page breaks.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 165

def arrange_block node, &block
  if @change_bar_attr && !scratch? &&
      (ARRANGED_BLOCK_CONTEXTS.include? node.context) && (change_bar? node)
    super node do |extent|
      if scratch?
        # measurement pass: upstream instance_execs this wrapper on the scratch
        # document, so re-bind the original block to it the same way
        instance_exec(&block)
      else
        block.call extent
        ink_change_bar_for_extent extent
      end
    end
  else
    super
  end
end

#bracket_change_bar(node) ⇒ Object

Brackets a block conversion with a before/after page-cursor snapshot and inks the bar across whatever it spanned, for converters that don't route through arrange_block: paragraphs and the three list types (convert_paragraph, convert_ulist, convert_olist, convert_dlist). Same shape as convert_table/convert_image, pulled out as a shared helper since none of the four need any special- casing the others don't.



190
191
192
193
194
195
196
197
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 190

def bracket_change_bar node
  if @change_bar_attr && !scratch? && (change_bar? node)
    bar_from = { page: page_number, cursor: cursor }
  end
  result = yield
  ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
  result
end

#change_bar?(node) ⇒ Boolean

Returns:

  • (Boolean)


52
53
54
55
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 52

def change_bar? node
  @change_bar_attr && node.respond_to?(:attributes) &&
    node.attributes && (node.attributes.key? @change_bar_attr)
end

#change_bar_settingsObject

Resolved lazily on first ink: the theme (and @media/@folio_placement) are not loaded yet when init_change_bars runs; upstream convert_document sets them up after rhrev's pre-super initialization. Precedence: document attribute > theme key > built-in default.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 37

def change_bar_settings
  @change_bar_settings ||= begin
    overrides = @change_bar_overrides || {}
    theme = @theme
    side = overrides[:side] || theme&.rhrev_change_bars_side ||
      (@media == 'prepress' ? 'outer' : 'right')
    {
      color: (overrides[:color] || theme&.rhrev_change_bars_color || 'FF0000').to_s.delete_prefix('#').upcase,
      width: (overrides[:width] || theme&.rhrev_change_bars_width || 2).to_f,
      offset: (overrides[:offset] || theme&.rhrev_change_bars_offset || 16).to_f,
      side: side.to_s,
    }
  end
end

#change_bar_x(pgnum, settings) ⇒ Object

The offset measures the gap between the text edge and the near edge of the bar. Sides recto/verso are determined by the physical page number (honoring pdf-folio-placement inversion), matching prepress physical folio placement.



114
115
116
117
118
119
120
121
122
123
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 114

def change_bar_x pgnum, settings
  case settings[:side]
  when 'right'
    on_right = true
  when 'outer', 'inner'
    recto = (page_side pgnum, @folio_placement && @folio_placement[:inverted]) == :recto
    on_right = settings[:side] == 'outer' ? recto : !recto
  end
  on_right ? bounds.right + settings[:offset] : bounds.left - settings[:offset] - settings[:width]
end

#enter_change_bar_section(node) ⇒ Object

A section's bar covers only its own directly-owned content, not any nested child sections/chapters: as soon as a child section begins rendering, close out the bar of every still-open ancestor section right here, at the boundary between the parent's own content and its first child section. A section carrying the recursive option keeps its bar open across child boundaries; convert_section closes it after the whole section, children included, has rendered.



95
96
97
98
99
100
101
102
103
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 95

def enter_change_bar_section node
  return unless @change_bar_attr
  @change_bar_section_stack.each do |ancestor|
    next if ancestor.option? 'recursive'
    next unless (start_pos = take_change_bar_start ancestor)
    ink_change_bar start_pos, { page: page_number, cursor: cursor }
  end
  @change_bar_section_stack.push node
end

#exit_change_bar_section(node) ⇒ Object



105
106
107
108
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 105

def exit_change_bar_section node
  return unless @change_bar_attr
  @change_bar_section_stack.pop
end

#init_change_bars(doc) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 14

def init_change_bars doc
  @change_bar_attr = nil
  @change_bar_settings = nil
  return unless doc.attr? 'rhrev-change-bars'
  # Without a revnumber there is no "current" revision to match
  return unless (revnumber = doc.attr 'revnumber')
  @change_bar_attr = %(#{@revision_prefix}#{revnumber.to_s.tr '.', '-'})
  # Capture attribute values only; holding the Document in an ivar here
  # would leak it into the scratch prototype, which must stay marshalable
  @change_bar_overrides = {
    color: (doc.attr 'rhrev-change-bars-color'),
    width: (doc.attr 'rhrev-change-bars-width'),
    offset: (doc.attr 'rhrev-change-bars-offset'),
    side: (doc.attr 'rhrev-change-bars-side'),
  }
  @change_bar_starts = {}
  @change_bar_section_stack = []
end

#ink_change_bar(from, to) ⇒ Object

Paints the bar page by page; from/to are { page:, cursor: }. Called immediately after the block is rendered, so all spanned pages exist.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 127

def ink_change_bar from, to
  return if scratch? || from.nil? || to.nil?
  return if to[:page] < from[:page]
  return if to[:page] == from[:page] && to[:cursor] >= from[:cursor]
  settings = change_bar_settings
  float do
    (from[:page]..to[:page]).each do |pgnum|
      go_to_page pgnum unless page_number == pgnum
      top = pgnum == from[:page] ? from[:cursor] : bounds.top
      bottom = pgnum == to[:page] ? to[:cursor] : 0
      next if (height = top - bottom) <= 0
      bounding_box [(change_bar_x pgnum, settings), top], width: settings[:width], height: height do
        fill_bounds settings[:color]
      end
    end
  end
end

#ink_change_bar_for_extent(extent) ⇒ Object



145
146
147
148
149
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 145

def ink_change_bar_for_extent extent
  return unless extent
  ink_change_bar({ page: extent.from.page, cursor: extent.from.cursor },
    { page: extent.to.page, cursor: extent.to.cursor })
end

#ink_chapter_title(node, title, opts = {}) ⇒ Object



207
208
209
210
211
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 207

def ink_chapter_title node, title, opts = {}
  record_change_bar_start node if @change_bar_attr && !scratch? && (change_bar? node)
  record_section_heading_position node unless scratch?
  super
end

#ink_general_heading(node, title, opts = {}) ⇒ Object

The heading ink methods run after arrange_heading / start_new_chapter have settled the page, so this is the first accurate start position.



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

def ink_general_heading node, title, opts = {}
  record_change_bar_start node if @change_bar_attr && !scratch? && (change_bar? node)
  record_section_heading_position node unless scratch?
  super
end

#ink_part_title(node, title, opts = {}) ⇒ Object

Upstream defines ink_part_title as an alias copy of ink_chapter_title, so it must be overridden separately



215
216
217
218
219
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 215

def ink_part_title node, title, opts = {}
  record_change_bar_start node if @change_bar_attr && !scratch? && (change_bar? node)
  record_section_heading_position node unless scratch?
  super
end

#record_change_bar_start(node) ⇒ Object



57
58
59
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 57

def record_change_bar_start node
  @change_bar_starts[node] = { page: page_number, cursor: cursor }
end

#record_section_heading_position(node) ⇒ Object

A section's own cursor position at heading-ink time, before its body (potentially spanning several pages) renders. Unconditional, not gated on change bars being enabled at all: Converter# convert_section's own revision-history dest needs this, pairing it with pdf-page-start (also captured at this same moment, upstream's own convert_section, right before the heading ink call this method wraps) so both fields describe the section's own start consistently. A page-spanning section's cursor read after the whole body renders belongs to wherever that body finished, not the section's own start page; two entries sharing the same start page then sort by a y value that was never on that page at all, reversing their true top-to-bottom order, confirmed directly against a real document.



78
79
80
81
82
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 78

def record_section_heading_position node
  return unless node.context == :section
  @section_heading_positions ||= {}
  @section_heading_positions[node] = cursor
end

#take_change_bar_start(node) ⇒ Object



61
62
63
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 61

def take_change_bar_start node
  @change_bar_starts&.delete node
end

#take_section_heading_position(node) ⇒ Object



84
85
86
# File 'lib/asciidoctor/rhrev/change_bars.rb', line 84

def take_section_heading_position node
  @section_heading_positions&.delete node
end