Class: Asciidoctor::ListsExtended::PDFConverterWithLists
- Inherits:
-
Object
- Object
- Asciidoctor::ListsExtended::PDFConverterWithLists
- 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
-
#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.
-
#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.
-
#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).
-
#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.
-
#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.
-
#initialize(*args) ⇒ PDFConverterWithLists
constructor
A new instance of PDFConverterWithLists.
-
#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.
-
#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).
-
#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.
-
#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.
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 |
# 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 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.
212 213 214 215 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 212 def add_outline_level(outline, sections, num_levels, ) filtered = sections.reject { |s| s.attr? 'list-exclude-from-outline' } super outline, filtered, num_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.
68 69 70 71 72 73 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 68 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.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 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 320 321 322 323 324 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 254 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.
222 223 224 225 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 222 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
202 203 204 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 202 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.
177 178 179 180 181 182 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 177 def ink_running_content(periphery, doc, *args) reinsert_virtual_list_sections doc super ensure 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.
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 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 83 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.
51 52 53 54 55 56 57 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 51 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.
186 187 188 189 190 191 192 |
# File 'lib/asciidoctor-lists-extended/pdf_converter.rb', line 186 def reinsert_virtual_list_sections(doc) @virtual_list_sections.each do |sect| parent = sect.parent || doc parent.blocks << sect if parent.respond_to?(:blocks) && !(parent.blocks.include? sect) parent.sections << sect if parent.respond_to?(:sections) && !(parent.sections.include? sect) end end |