Class: Asciidoctor::ListsExtended::PDFConverterWithLists

Inherits:
Object
  • Object
show all
Includes:
PdfRenderer
Defined in:
lib/asciidoctor-lists-extended/pdf_converter.rb

Overview

Single unified PDF converter that replaces the four copy-pasted classes (PDFConverterWithLOF, PDFConverterWithLOT, PDFConverterWithLOE, PDFConverterWithLOL) from asciidoctor-pdf-lofte.

Hooks into asciidoctor-pdf's allocate_toc / ink_toc lifecycle to:

1. Reserve page space for each list-of:: macro found in the document
 (allocate_toc → allocate_list via dry_run).
2. Render each list with ToC-style dot leaders (ink_toc → ink_list).
3. Insert virtual Section nodes so lists appear in the PDF ToC and
 bookmark outline by default. Per-macro flags exclude_from_toc and
 exclude_from_outline opt individual lists out.

ink_toc_level is overridden only for list rendering (guarded by

Instance Method Summary collapse

Methods included from PdfRenderer

#get_list_entries, #ink_list_content, #insert_list_into_toc_section, #resolve_caption_style, #resolve_exclude_from_outline, #resolve_exclude_from_toc

Constructor Details

#initialize(*args) ⇒ PDFConverterWithLists

Returns a new instance of PDFConverterWithLists.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 28

def initialize(*args)
  super
  @list_extents                  = {}   # UUID → Extent (front-matter lists only)
  @list_configs_ordered          = []   # front-matter list configs in document order
  @inline_list_configs           = {}   # UUID → config for section-scoped inline lists
  @inline_render_positions       = {}   # UUID → deferred render position (page + cursor)
  @inline_num_front_matter_pages = 0    # saved from ink_toc for inline page-number math
  @rendering_list                = false
  @list_strip_period             = false
  @list_split_caption            = false
  @list_entry_indent             = nil
  @list_first_entry_margin       = nil
  @list_toc_insert_idx           = 0
  @virtual_list_sections         = []   # list-of:: virtual sections, kept for running content
  @virtual_section_indices       = {}   # section → original index in parent.blocks
  @toc_only_extent               = nil  # extent of the real ToC pages, without list-of pages
end

Instance Method Details

#add_outline(doc, *args) ⇒ Object


add_outline override

asciidoctor-pdf builds the PDF bookmark outline from doc.sections after ink_toc has returned, i.e. after ink_toc's ensure already purged the

list-of

virtual sections. Re-insert them for the duration of the

outline build (same compensating pattern as ink_running_content), then purge again. add_outline_level's list-exclude-from-outline filter still applies to the reinserted sections.



216
217
218
219
220
221
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 216

def add_outline(doc, *args)
  reinsert_virtual_list_sections doc
  super
ensure
  purge_virtual_sections doc
end

#add_outline_level(outline, sections, num_levels, expand_levels) ⇒ Object


add_outline_level override

Filters out virtual list sections marked with 'list-exclude-from-outline' before delegating to the parent, which builds PDF bookmarks. exclude_from_outline keeps a list in the ToC while hiding it from the outline.



259
260
261
262
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 259

def add_outline_level(outline, sections, num_levels, expand_levels)
  filtered = sections.reject { |s| s.attr? 'list-exclude-from-outline' }
  super outline, filtered, num_levels, expand_levels
end

#allocate_toc(doc, num_levels, toc_start_cursor, break_after_toc) ⇒ Object


allocate_toc hook

Called by asciidoctor-pdf early in document generation to reserve page

space for the ToC. We mark sections that contain empty list-of

macros

before calling super so that super's dry-run of ink_toc does not count those sections as ToC entries and over-allocate ToC pages. Those sections are removed entirely by collect_and_allocate_lists, so the marker never reaches the final document.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 70

def allocate_toc(doc, num_levels, toc_start_cursor, break_after_toc)
  mark_empty_list_sections doc
  result = super
  collect_and_allocate_lists doc, num_levels, toc_start_cursor, break_after_toc
  # The extent stored as @toc_extent must cover the list-of pages inserted
  # after the ToC, otherwise page-numbering-start-at/running-content-start-at
  # 'after-toc' and the PDF page labels treat those pages as body content.
  # Keep the unextended extent so ink_running_content can hand core the real
  # ToC page range (core stamps the toc-title on every page of @toc_extent).
  @toc_only_extent = result
  if result && !@list_extents.empty?
    last_to = @list_extents.values.map(&:to).max_by { |pos| [pos.page, -(pos.cursor || 0)] }
    if last_to.page > result.to.page ||
        (last_to.page == result.to.page && (last_to.cursor || 0) < (result.to.cursor || 0))
      result = result.dup
      result.to = last_to.dup
    end
  end
  result
end

#convert_paragraph(node) ⇒ Object


convert_paragraph override

Intercepts UUID placeholder paragraphs belonging to chapter-scoped inline lists (those stored in @inline_list_configs by collect_and_allocate_lists). Renders the list entries at the current cursor position without a heading (the enclosing === section provides the heading) and without any page break before or after. All other paragraphs delegate to super unchanged.

convert_paragraph override

Intercepts UUID placeholder paragraphs for section-scoped inline lists.

Rendering strategy (two-phase):

Phase 1 — body rendering (here, scratch? may be true or false):
• When scratch? is true: call ink_toc_level in scratch mode so Prawn
  measures the correct space for page-break decisions.
• When scratch? is false: dry-run the list to measure its height,
  reserve that space in the document by advancing the cursor, and
  record the page + cursor position in @inline_render_positions.
  No visible content is written yet.

Phase 2 — ink_toc (called by asciidoctor-pdf AFTER traverse doc):
By the time ink_toc runs all pdf-page-start attributes have been set.
For each saved position, navigate there and render with real dot leaders
and page numbers.  See ink_toc below.


301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 301

def convert_paragraph(node)
  if node.content_model == :simple && node.lines.size == 1 &&
     (config = @inline_list_configs[node.lines[0]])
    uuid    = node.lines[0]
    entries = get_list_entries(config[:scope_node], config[:element])

    unless entries.empty?
      entries.each { |e| e.level = 2 if e.title }
      num_levels = @theme.toc_levels || 2
      dot_leader = build_dot_leader_config(num_levels)

      caption_style      = resolve_caption_style(config)
      entry_indent       = config[:entry_indent] || @theme.list_of_entry_indent
      first_entry_margin = config[:first_entry_margin] || @theme.list_of_first_entry_margin

      if scratch?
        # Scratch pass (e.g. heading orphan detection): measure space only.
        begin
          @rendering_list          = true
          @list_strip_period       = (caption_style == 'strip')
          @list_split_caption      = (caption_style == 'split')
          @list_entry_indent       = entry_indent
          @list_first_entry_margin = first_entry_margin
          ink_toc_level entries, num_levels, dot_leader, 0
        ensure
          @rendering_list          = false
          @list_strip_period       = false
          @list_split_caption      = false
          @list_entry_indent       = nil
          @list_first_entry_margin = nil
        end
      else
        # Real pass: measure via dry_run, reserve blank space, save position.
        extent = dry_run(onto: self) do
          begin
            @rendering_list          = true
            @list_strip_period       = (caption_style == 'strip')
            @list_split_caption      = (caption_style == 'split')
            @list_entry_indent       = entry_indent
            @list_first_entry_margin = first_entry_margin
            ink_toc_level entries, num_levels, dot_leader, 0
          ensure
            @rendering_list          = false
            @list_strip_period       = false
            @list_split_caption      = false
            @list_entry_indent       = nil
            @list_first_entry_margin = nil
          end
        end

        save_page   = extent.from.page
        save_cursor = extent.from.cursor

        # Reserve the space without rendering any content.
        extent.each_page { |first_page| start_new_page unless first_page }
        move_cursor_to extent.to.cursor

        @inline_render_positions[uuid] = {
          config:     config,
          entries:    entries,
          num_levels: num_levels,
          dot_leader: dot_leader,
          page:       save_page,
          cursor:     save_cursor,
        }
      end
    end
    return
  end
  super
end

#convert_section(node, *args) ⇒ Object


convert_section override

Virtual sections inserted only to drive ToC/outline entries should not be rendered as normal body headings during document traversal.



269
270
271
272
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 269

def convert_section(node, *args)
  return if node.attr? 'list-virtual-section'
  super node, *args
end

#get_entries_for_toc(node) ⇒ Object


get_entries_for_toc override

Filters out:

list-exclude-from-toc     — virtual list sections the author opted out of the ToC
list-toc-measure-skip     — original sections for empty lists, temporarily marked
                          by mark_empty_list_sections so allocate_toc's dry-run
                          does not over-allocate ToC pages for them


249
250
251
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 249

def get_entries_for_toc(node)
  super.reject { |s| (s.attr? 'list-exclude-from-toc') || (s.attr? 'list-toc-measure-skip') }
end

#ink_running_content(periphery, doc, *args) ⇒ Object


ink_running_content override

asciidoctor-pdf builds its per-page running-content title map from doc.find_by(context: :section), keyed by each section's pdf-page-start.

Our list-of

virtual sections carry both a title and a pdf-page-start,

but they were purged in ink_toc's ensure (which runs before this), so the list-of pages fall back to the preface/chapter title. Re-insert them for the duration of the indexing pass, then purge again so they cannot leak into the scratch/final document tree.



193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 193

def ink_running_content(periphery, doc, *args)
  # Core stamps the toc-title on every page covered by @toc_extent, which
  # now also spans the list-of pages (see allocate_toc).  Swap in the
  # ToC-only extent so the list-of pages keep their own running titles
  # (supplied by the reinserted virtual sections below).
  widened_toc_extent = @toc_extent
  @toc_extent = @toc_only_extent if @toc_only_extent
  reinsert_virtual_list_sections doc
  super
ensure
  @toc_extent = widened_toc_extent
  purge_virtual_sections doc
end

#ink_toc(doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0) ⇒ Object


ink_toc hook

Render every allocated list before delegating to super (which renders the real ToC). Always inserts a virtual Section node for each list so it appears in the PDF bookmark outline. exclude_from_toc marks the section so get_entries_for_toc filters it from the visible ToC; exclude_from_outline marks it so add_outline_level skips it.



99
100
101
102
103
104
105
106
107
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 99

def ink_toc(doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0)
  @inline_num_front_matter_pages = num_front_matter_pages
  @list_toc_insert_idx = 0
  @virtual_list_sections = []

  # toc-in-toc: insert the Table of Contents itself as the first entry in
  # the PDF ToC, before any list-of:: sections.  Excluded from the bookmark
  # outline because asciidoctor-pdf's add_outline already adds a ToC bookmark.
  # IMPORTANT: do this in both scratch and final passes. During scratch,
  # @toc_extent may not yet be set; fall back to toc_page_number so the
  # dry-run ToC entry count/height matches the final pass.
  if doc.attr? 'toc-in-toc'
    toc_entry_title = doc.attr('toc-title') || 'Table of Contents'
    toc_entry_range = @toc_extent&.page_range || (toc_page_number..toc_page_number)
    insert_list_into_toc_section doc, toc_entry_title, toc_entry_range,
      '_toc_in_toc', @list_toc_insert_idx,
      pdf_dest: (@toc_extent ? dest_top(@toc_extent.page_range.first) : nil),
      exclude_from_outline: true
    @list_toc_insert_idx += 1
  end

  @list_configs_ordered.each do |config|
    extent = @list_extents[config[:uuid]]
    next unless extent

    ink_list doc, config, extent, num_levels, num_front_matter_pages

    list_title     = config[:title] || config[:section_title]
    list_page_nums = extent.page_range
    list_id        = "_list_of_#{config[:element].tr('-', '_')}"
    # Bare macros (no parent section and no title= attribute) have no
    # meaningful label for the ToC or outline — skip the virtual section.
    unless list_title.nil_or_empty?
      virtual_section = insert_list_into_toc_section doc, list_title, list_page_nums, list_id, @list_toc_insert_idx,
        pdf_dest:             @last_list_dest || dest_top(list_page_nums.first),
        exclude_from_toc:     resolve_exclude_from_toc(config),
        exclude_from_outline: resolve_exclude_from_outline(config)
      # Retain a reference so running content can show the list title on its
      # own pages; the section itself is purged from the tree below.
      @virtual_list_sections << virtual_section
      @list_toc_insert_idx += 1
    end
  end

  result = super

  # Phase 2: render deferred inline section-scoped lists.

  # traverse doc has completed before ink_toc is called, so all
  # pdf-page-start attributes are now set and page numbers are real.
  unless @inline_render_positions.empty?
    @inline_render_positions.each_value do |pos|
      go_to_page pos[:page]
      move_cursor_to pos[:cursor]
      entries = get_list_entries(pos[:config][:scope_node], pos[:config][:element])
      next if entries.empty?
      entry_level = (@theme.list_of_entry_level || 2).to_i
      entries.each { |e| e.level = entry_level if e.title }
      caption_style    = resolve_caption_style(pos[:config])
      entry_indent     = pos[:config][:entry_indent] || @theme.list_of_entry_indent
      first_entry_margin = pos[:config][:first_entry_margin] || @theme.list_of_first_entry_margin
      begin
        @rendering_list          = true
        @list_strip_period       = (caption_style == 'strip')
        @list_split_caption      = (caption_style == 'split')
        @list_entry_indent       = entry_indent
        @list_first_entry_margin = first_entry_margin
        ink_toc_level entries, pos[:num_levels], pos[:dot_leader], num_front_matter_pages
      ensure
        @rendering_list          = false
        @list_strip_period       = false
        @list_split_caption      = false
        @list_entry_indent       = nil
        @list_first_entry_margin = nil
      end
    end
    go_to_page page_count
  end

  result
ensure
  purge_virtual_sections doc
end

#ink_toc_level(entries, num_levels, dot_leader, num_front_matter_pages) ⇒ Object


ink_toc_level override

When @rendering_list is true (set by PdfRenderer#ink_list_content), dispatch to our list-specific implementation that uses captioned_title for non-section entries. Otherwise delegate to the parent unchanged so that the real Table of Contents is not affected.



53
54
55
56
57
58
59
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 53

def ink_toc_level(entries, num_levels, dot_leader, num_front_matter_pages)
  if @rendering_list
    ink_list_toc_level entries, num_levels, dot_leader, num_front_matter_pages
  else
    super
  end
end

#reinsert_virtual_list_sections(doc) ⇒ Object

Restore the retained list-of

virtual sections into their original

parent so doc.find_by(context: :section) can see them again. Each section goes back to the block index recorded by purge_virtual_sections; position matters because doc.sections (derived from blocks order) drives the PDF bookmark outline order in add_outline.



228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 228

def reinsert_virtual_list_sections(doc)
  @virtual_list_sections.each do |sect|
    parent = sect.parent || doc
    next unless parent.respond_to?(:blocks) && !(parent.blocks.include? sect)
    idx = @virtual_section_indices[sect]
    if idx && idx <= parent.blocks.size
      parent.blocks.insert idx, sect
    else
      parent.blocks << sect
    end
  end
end