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
# 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
end

Instance Method Details

#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.



173
174
175
176
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 173

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.



67
68
69
70
71
72
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 67

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
  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.


205
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
271
272
273
274
275
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 205

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

#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


163
164
165
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 163

def get_entries_for_toc(node)
  super.reject { |s| (s.attr? 'list-exclude-from-toc') || (s.attr? 'list-toc-measure-skip') }
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.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 82

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

  # 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.
  if (doc.attr? 'toc-in-toc') && @toc_extent
    toc_entry_title = doc.attr('toc-title') || 'Table of Contents'
    insert_list_into_toc_section doc, toc_entry_title, @toc_extent.page_range,
      '_toc_in_toc', @list_toc_insert_idx,
      pdf_dest: dest_top(@toc_extent.page_range.first),
      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?
      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)
      @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
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.



50
51
52
53
54
55
56
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 50

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