asciidoctor-lists-extended 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1f0396e93370444193a1258530dd32f81db35dddaedc4c911a31655d94d5e2c
4
- data.tar.gz: bbd4f0412e6b11fcfc1bb39b38a2cd9932c8738a740d8c6b693f4185c03c0566
3
+ metadata.gz: 2fef48386d4e8e43dc4b346f18f0eacc159e4660ae72a37b7d0e309bcee7f1b5
4
+ data.tar.gz: 794aa84d51d840cfcf72deea61a208030070c9efc0779d1ee70c4bdf2997f655
5
5
  SHA512:
6
- metadata.gz: 9970ef9f04375ee8e446abb219171880806ac09bdc09f1909f0830eda569e37191188cd195813185339e56b31ccaaf6446f835415f77d5adaf912b6bf032aee3
7
- data.tar.gz: fe43e888bba80b85b9212f24df7bed543224e510f22e4b40aefd754eeacf27b0e5e22220d2bc9a3d70e684dacab1e00d49ab120547b74382b6ad9bdc86bda824
6
+ metadata.gz: e2a3f7a9859c3fcb6a83b552b3a23138cc992c88349d9ea2202541cffc46425e4bb579fe8fe04cb742421c9810e6a76d96260fb8ae3f5fa170aeef27a48d0bb7
7
+ data.tar.gz: d0821487c0d6510278fa8148a3c615682657228b61d9052b1460de9f23e45f789def711dfa0834da30d46adeadde73a3be77f366fd3a1bba1858861c88d50f41
data/README.adoc CHANGED
@@ -112,6 +112,14 @@ Kept as an explicit opt-in for backwards compatibility with the original `asciid
112
112
 
113
113
  |`exclude_from_outline`
114
114
  |PDF only. Omits this list from the PDF bookmark outline while keeping it in the Table of Contents.
115
+
116
+ |`strip_period`
117
+ |PDF only. Removes the trailing period from caption signifiers, e.g. "Table 1." → "Table 1".
118
+ Overrides the theme `caption_style` for this macro (equivalent to `caption_style: strip`).
119
+
120
+ |`split_caption`
121
+ |PDF only. Renders the caption signifier (e.g. "Table 1") at the left margin and the title indented at the `entry_indent` offset.
122
+ Overrides the theme `caption_style` for this macro (equivalent to `caption_style: split`).
115
123
  |===
116
124
 
117
125
  === Named Parameters
@@ -127,6 +135,14 @@ Kept as an explicit opt-in for backwards compatibility with the original `asciid
127
135
  |`caption_prefix`
128
136
  |Both
129
137
  |Override the caption prefix label (e.g. `"Figure"`). Currently informational; caption is taken from the element's own `captioned_title`.
138
+
139
+ |`entry_indent`
140
+ |PDF
141
+ |Numeric (pt). Override the theme `entry_indent` for this macro only.
142
+
143
+ |`first_entry_margin`
144
+ |PDF
145
+ |Numeric (pt). Override the theme `first_entry_margin` for this macro only.
130
146
  |===
131
147
 
132
148
  === Supported Element Types
@@ -157,6 +173,18 @@ Elements *without* a title or caption are silently skipped.
157
173
 
158
174
  == Document Attributes
159
175
 
176
+ === Extension Attributes
177
+
178
+ [cols="1,3"]
179
+ |===
180
+ |Attribute |Effect
181
+
182
+ |`:toc-in-toc:`
183
+ |PDF only. Inserts the Table of Contents itself as the first entry in the PDF ToC and bookmark outline, before any `list-of::` sections.
184
+ The entry title is taken from the `:toc-title:` attribute (default: `Table of Contents`).
185
+
186
+ |===
187
+
160
188
  === Caption Attributes (standard Asciidoctor)
161
189
 
162
190
  [source,asciidoc]
@@ -179,9 +207,12 @@ In PDF mode the extension hooks into `asciidoctor-pdf`'s ToC allocation/renderin
179
207
 
180
208
  === Placement
181
209
 
182
- All lists are placed in the *front matter* (directly after the ToC) regardless of where the `list-of::` macros appear in the source.
210
+ Document-wide lists (macros inside a top-level `== …` section) are placed in the *front matter* (directly after the ToC) regardless of where in the source the `list-of::` macros appear.
183
211
  The macro order in the source determines the order of the lists in the front matter.
184
212
 
213
+ When the top-level section that contains a `list-of::` macro also has other content — admonitions, paragraphs, or other blocks — those blocks are moved to the front matter too and rendered between the list heading and the first entry.
214
+ The enclosing section is then removed from the body so it does not appear twice.
215
+
185
216
  You can place `list-of::` macros inside any subsection to generate *section-scoped* lists.
186
217
  The scope is determined automatically from where the macro appears in the document hierarchy:
187
218
 
@@ -254,6 +285,65 @@ image_list_title:
254
285
  text_align: center
255
286
  ----
256
287
 
288
+ === Theme Keys (`asciidoctor_lists_extended`)
289
+
290
+ List entry rendering can be configured globally via your PDF theme YAML under the `asciidoctor_lists_extended` namespace.
291
+ Macro-level attributes always override theme values.
292
+
293
+ [source,yaml]
294
+ ----
295
+ asciidoctor_lists_extended:
296
+ caption_style: split # default | split | strip
297
+ entry_indent: 58 # pt — left indent for entry title text
298
+ first_entry_margin: 10 # pt — vertical space before the first entry
299
+ image:
300
+ exclude_from_toc: false # default exclude for list-of::image[]
301
+ exclude_from_outline: false
302
+ table:
303
+ exclude_from_toc: false
304
+ exclude_from_outline: false
305
+ listing:
306
+ exclude_from_toc: false
307
+ exclude_from_outline: false
308
+ example:
309
+ exclude_from_toc: false
310
+ exclude_from_outline: false
311
+ ----
312
+
313
+ [cols="2,1,4"]
314
+ |===
315
+ |Key |Default |Description
316
+
317
+ |`caption_style`
318
+ |`default`
319
+ |Controls how captioned list entries are rendered. +
320
+ `default` — full `captioned_title` as one string. +
321
+ `split` — signifier (e.g. "Table 1") at left margin, title indented at `entry_indent`. +
322
+ `strip` — full title with trailing period removed from the signifier.
323
+
324
+ |`entry_indent`
325
+ |`0`
326
+ |Left indent (pt) for entry text.
327
+ In `split` mode the signifier stays at 0 and the title starts at this offset.
328
+ In `default`/`strip` modes the entire entry is indented.
329
+
330
+ |`first_entry_margin`
331
+ |`0`
332
+ |Extra vertical space (pt) inserted before the first entry of each list.
333
+
334
+ |`<element>.exclude_from_toc`
335
+ |`false`
336
+ |Per-element-type default for omitting lists from the PDF Table of Contents.
337
+ Overridden by the `exclude_from_toc` positional flag on individual macros.
338
+
339
+ |`<element>.exclude_from_outline`
340
+ |`false`
341
+ |Per-element-type default for omitting lists from the PDF bookmark outline.
342
+ Overridden by the `exclude_from_outline` positional flag on individual macros.
343
+ |===
344
+
345
+ **Override priority:** macro attribute > theme key > built-in default.
346
+
257
347
  == HTML Behaviour
258
348
 
259
349
  In HTML5 mode the macro is replaced *in-place* with a list of cross-reference links:
@@ -301,12 +391,6 @@ image::arch.png[]
301
391
  |timeout |30 |Seconds
302
392
  |===
303
393
 
304
- .bootstrap.sh
305
- [source,bash]
306
- ----
307
- #!/bin/bash
308
- echo "Starting..."
309
- ----
310
394
  ----
311
395
  <1> Plain usage — collects all images with captions. Appears in PDF ToC and outline by default.
312
396
  <2> If no tables exist, the "List of Tables" section is omitted entirely.
@@ -368,7 +452,12 @@ The `list-of::` macros render as `<ul>` lists with `<a href="#…">` links.
368
452
 
369
453
  |`:include-lists-in-toc:`
370
454
  |Lists are included in the PDF ToC and bookmark outline by default.
371
- Use `exclude_from_toc` or `exclude_from_outline` on individual macros to opt out.
455
+ Use `exclude_from_toc` or `exclude_from_outline` on individual macros to opt out, or set per-element-type defaults via theme keys.
456
+ To add "Table of Contents" itself as the first entry in the ToC/outline, set `:toc-in-toc:`.
457
+
458
+ |Hardcoded line height / spacing / indentation in lofte converter
459
+ |Theme-driven via `asciidoctor_lists_extended` YAML keys (`caption_style`, `entry_indent`, `first_entry_margin`).
460
+ Macro-level overrides (`split_caption`, `strip_period`, `entry_indent=X`, `first_entry_margin=X`) still available.
372
461
 
373
462
  |Required `[#anchor]` on every block
374
463
  |Auto-generated — no anchors needed
@@ -377,7 +466,13 @@ Use `exclude_from_toc` or `exclude_from_outline` on individual macros to opt out
377
466
  |HTML5 + PDF
378
467
 
379
468
  |4 copy-pasted converter classes (~1200 lines)
380
- |1 unified converter class (~320 lines)
469
+ |1 unified converter class (~550 lines)
470
+
471
+ |`PDFConverterModifyRunningContent` (custom running content)
472
+ |Virtual sections get `pdf-page-start` automatically — base `ink_running_content` picks up list titles as `{chapter-title}`.
473
+
474
+ |`ModifyOutline` (custom outline builder)
475
+ |`add_outline_level` override filters excluded lists and strips trailing dots from section numbers.
381
476
  |===
382
477
 
383
478
  == Architecture Overview
@@ -447,6 +542,27 @@ Keeping the drawing logic in a separate mixin makes that reuse clean: the conver
447
542
  . *`add_outline_level` override* (`pdf_converter.rb`) — called by asciidoctor-pdf when building the PDF bookmark outline.
448
543
  Filters out any virtual list sections marked with `list-exclude-from-outline` before delegating to `super`, implementing the `exclude_from_outline` flag.
449
544
 
545
+ == Known Limitations
546
+
547
+ `toc-placement: macro`::
548
+ When `:toc-placement: macro` is set, asciidoctor-pdf defers its `allocate_toc` call to
549
+ the inline `toc::[]` macro during body rendering.
550
+ This extension's `allocate_toc` override therefore never runs, so UUID placeholder paragraphs
551
+ are not processed and appear as raw text in the output.
552
+ Use the default `:toc:` attribute (which places the ToC at the top of the front matter)
553
+ instead.
554
+
555
+ Bare macros excluded from ToC and outline::
556
+ When a `list-of::` macro is placed outside any section heading (document preamble), the
557
+ generated list has no title and is therefore excluded from the PDF Table of Contents and
558
+ bookmark outline.
559
+ Wrap the macro in a `==` section to give it a title and have it appear in the ToC.
560
+
561
+ CJK font support::
562
+ asciidoctor-pdf does not bundle CJK fonts.
563
+ Documents using Chinese, Japanese, or Korean text require a custom PDF theme that specifies
564
+ a CJK-capable font family.
565
+
450
566
  == Licence
451
567
 
452
568
  MIT
@@ -18,8 +18,12 @@ module Asciidoctor
18
18
  # hide_empty_section — remove the parent section if no entries found (positional)
19
19
  # exclude_from_toc — omit this list from the PDF Table of Contents and outline (positional, PDF only)
20
20
  # exclude_from_outline — omit this list from the PDF bookmark outline only (positional, PDF only)
21
+ # strip_period — remove the trailing period from caption signifiers, e.g. "Table 1." → "Table 1" (positional)
22
+ # split_caption — render signifier and title separately with indentation in list entries (positional, PDF only)
21
23
  # caption_prefix — override the caption prefix (e.g. "Figure")
22
24
  # title — override the list heading
25
+ # entry_indent — (named, numeric) override theme entry indent (pt)
26
+ # first_entry_margin — (named, numeric) override theme first-entry margin (pt)
23
27
  #
24
28
  # Example:
25
29
  # list-of::image[]
@@ -39,8 +43,12 @@ module Asciidoctor
39
43
  hide_empty_section: attrs['hide_empty_section'] || pos_flags.include?('hide_empty_section'),
40
44
  exclude_from_toc: attrs['exclude_from_toc'] || pos_flags.include?('exclude_from_toc'),
41
45
  exclude_from_outline: attrs['exclude_from_outline'] || pos_flags.include?('exclude_from_outline'),
46
+ strip_period: attrs['strip_period'] || pos_flags.include?('strip_period'),
47
+ split_caption: attrs['split_caption'] || pos_flags.include?('split_caption'),
42
48
  caption_prefix: attrs['caption_prefix'],
43
49
  title: attrs['title'],
50
+ entry_indent: attrs['entry_indent']&.to_f,
51
+ first_entry_margin: attrs['first_entry_margin']&.to_f,
44
52
  }
45
53
  create_paragraph parent, uuid, {}
46
54
  end
@@ -68,6 +76,7 @@ module Asciidoctor
68
76
  .find_by(traverse_documents: true, context: config[:element].to_sym)
69
77
  .each do |element|
70
78
  next unless element.caption || element.title
79
+ next if element.style == 'discrete'
71
80
  next if element.id
72
81
 
73
82
  element.id = SecureRandom.uuid
@@ -27,23 +27,27 @@ module Asciidoctor
27
27
  context: params[:element].to_sym
28
28
  )
29
29
 
30
- titled_elements = elements.select { |e| e.caption || e.title }
30
+ titled_elements = elements.select { |e| (e.caption || e.title) && e.style != 'discrete' }
31
31
 
32
32
  if titled_elements.empty? && hide_empty_section
33
33
  block.parent.parent.blocks.delete(block.parent)
34
34
  next
35
35
  end
36
36
 
37
+ strip_period = params[:strip_period]
38
+
37
39
  references_asciidoc = titled_elements.map do |element|
40
+ caption_text = element.caption&.rstrip
41
+ caption_text = caption_text.sub(/\.\z/, '') if strip_period && caption_text
38
42
  if enhanced_rendering
39
- if element.caption
40
- %(xref:#{element.id}[#{element.caption.rstrip}] #{element.instance_variable_get(:@title)} +)
43
+ if caption_text
44
+ %(xref:#{element.id}[#{caption_text}] #{element.instance_variable_get(:@title)} +)
41
45
  else
42
46
  %(xref:#{element.id}[#{element.instance_variable_get(:@title)}] +)
43
47
  end
44
48
  else
45
- if element.caption
46
- %(xref:#{element.id}[#{element.caption.rstrip}] #{element.title} +)
49
+ if caption_text
50
+ %(xref:#{element.id}[#{caption_text}] #{element.title} +)
47
51
  else
48
52
  %(xref:#{element.id}[#{element.title}] +)
49
53
  end
@@ -33,6 +33,10 @@ module Asciidoctor
33
33
  @inline_render_positions = {} # UUID → deferred render position (page + cursor)
34
34
  @inline_num_front_matter_pages = 0 # saved from ink_toc for inline page-number math
35
35
  @rendering_list = false
36
+ @list_strip_period = false
37
+ @list_split_caption = false
38
+ @list_entry_indent = nil
39
+ @list_first_entry_margin = nil
36
40
  @list_toc_insert_idx = 0
37
41
  end
38
42
 
@@ -75,6 +79,17 @@ module Asciidoctor
75
79
  @inline_num_front_matter_pages = num_front_matter_pages
76
80
  @list_toc_insert_idx = 0
77
81
 
82
+ # toc-in-toc: insert the Table of Contents itself as the first entry in
83
+ # the PDF ToC, before any list-of:: sections. Excluded from the bookmark
84
+ # outline because asciidoctor-pdf's add_outline already adds a ToC bookmark.
85
+ if (doc.attr? 'toc-in-toc') && @toc_extent
86
+ toc_entry_title = doc.attr('toc-title') || 'Table of Contents'
87
+ insert_list_into_toc_section doc, toc_entry_title, @toc_extent.page_range,
88
+ '_toc_in_toc', @list_toc_insert_idx,
89
+ exclude_from_outline: true
90
+ @list_toc_insert_idx += 1
91
+ end
92
+
78
93
  @list_configs_ordered.each do |config|
79
94
  extent = @list_extents[config[:uuid]]
80
95
  next unless extent
@@ -84,10 +99,14 @@ module Asciidoctor
84
99
  list_title = config[:title] || config[:section_title]
85
100
  list_page_nums = extent.page_range
86
101
  list_id = "_list_of_#{config[:element].tr('-', '_')}"
87
- insert_list_into_toc_section doc, list_title, list_page_nums, list_id, @list_toc_insert_idx,
88
- exclude_from_toc: config[:exclude_from_toc],
89
- exclude_from_outline: config[:exclude_from_outline]
90
- @list_toc_insert_idx += 1
102
+ # Bare macros (no parent section and no title= attribute) have no
103
+ # meaningful label for the ToC or outline — skip the virtual section.
104
+ unless list_title.nil_or_empty?
105
+ insert_list_into_toc_section doc, list_title, list_page_nums, list_id, @list_toc_insert_idx,
106
+ exclude_from_toc: resolve_exclude_from_toc(config),
107
+ exclude_from_outline: resolve_exclude_from_outline(config)
108
+ @list_toc_insert_idx += 1
109
+ end
91
110
  end
92
111
 
93
112
  result = super
@@ -102,11 +121,22 @@ module Asciidoctor
102
121
  entries = get_list_entries(pos[:config][:scope_node], pos[:config][:element])
103
122
  next if entries.empty?
104
123
  entries.each { |e| e.level = 2 if e.title }
124
+ caption_style = resolve_caption_style(pos[:config])
125
+ entry_indent = pos[:config][:entry_indent] || @theme.asciidoctor_lists_extended_entry_indent
126
+ first_entry_margin = pos[:config][:first_entry_margin] || @theme.asciidoctor_lists_extended_first_entry_margin
105
127
  begin
106
- @rendering_list = true
128
+ @rendering_list = true
129
+ @list_strip_period = (caption_style == 'strip')
130
+ @list_split_caption = (caption_style == 'split')
131
+ @list_entry_indent = entry_indent
132
+ @list_first_entry_margin = first_entry_margin
107
133
  ink_toc_level entries, pos[:num_levels], pos[:dot_leader], num_front_matter_pages
108
134
  ensure
109
- @rendering_list = false
135
+ @rendering_list = false
136
+ @list_strip_period = false
137
+ @list_split_caption = false
138
+ @list_entry_indent = nil
139
+ @list_first_entry_margin = nil
110
140
  end
111
141
  end
112
142
  go_to_page page_count
@@ -132,8 +162,29 @@ module Asciidoctor
132
162
  # before delegating to the parent implementation, which builds PDF bookmarks.
133
163
  # This lets exclude_from_outline keep a list in the ToC while hiding it
134
164
  # from the PDF bookmark outline.
165
+ #
166
+ # Also strips trailing dots from numbered section titles so outline entries
167
+ # read "1.2.3 Title" instead of "1.2.3. Title" (ported from the former
168
+ # ModifyOutline converter).
135
169
  def add_outline_level(outline, sections, num_levels, expand_levels)
136
- super outline, sections.reject { |s| s.attr? 'list-exclude-from-outline' }, num_levels, expand_levels
170
+ filtered = sections.reject { |s| s.attr? 'list-exclude-from-outline' }
171
+ patched_sects = []
172
+ filtered.each do |sect|
173
+ next unless sect.context == :section
174
+ raw_title = sect.numbered_title(formal: true)
175
+ cleaned = if raw_title =~ /\A([\d.]+)\.\s+(.*)\z/
176
+ "#{$1} #{$2}"
177
+ elsif raw_title =~ /\A([\d.]+)\.\z/
178
+ $1
179
+ end
180
+ if cleaned
181
+ sect.define_singleton_method(:numbered_title) { |**_opts| cleaned }
182
+ patched_sects << sect
183
+ end
184
+ end
185
+ super outline, filtered, num_levels, expand_levels
186
+ ensure
187
+ patched_sects&.each { |s| s.singleton_class.send(:remove_method, :numbered_title) }
137
188
  end
138
189
 
139
190
  # -----------------------------------------------------------------------
@@ -174,22 +225,42 @@ module Asciidoctor
174
225
  num_levels = @theme.toc_levels || 2
175
226
  dot_leader = build_dot_leader_config(num_levels)
176
227
 
228
+ caption_style = resolve_caption_style(config)
229
+ entry_indent = config[:entry_indent] || @theme.asciidoctor_lists_extended_entry_indent
230
+ first_entry_margin = config[:first_entry_margin] || @theme.asciidoctor_lists_extended_first_entry_margin
231
+
177
232
  if scratch?
178
233
  # Scratch pass (e.g. heading orphan detection): measure space only.
179
234
  begin
180
- @rendering_list = true
235
+ @rendering_list = true
236
+ @list_strip_period = (caption_style == 'strip')
237
+ @list_split_caption = (caption_style == 'split')
238
+ @list_entry_indent = entry_indent
239
+ @list_first_entry_margin = first_entry_margin
181
240
  ink_toc_level entries, num_levels, dot_leader, 0
182
241
  ensure
183
- @rendering_list = false
242
+ @rendering_list = false
243
+ @list_strip_period = false
244
+ @list_split_caption = false
245
+ @list_entry_indent = nil
246
+ @list_first_entry_margin = nil
184
247
  end
185
248
  else
186
249
  # Real pass: measure via dry_run, reserve blank space, save position.
187
250
  extent = dry_run(onto: self) do
188
251
  begin
189
- @rendering_list = true
252
+ @rendering_list = true
253
+ @list_strip_period = (caption_style == 'strip')
254
+ @list_split_caption = (caption_style == 'split')
255
+ @list_entry_indent = entry_indent
256
+ @list_first_entry_margin = first_entry_margin
190
257
  ink_toc_level entries, num_levels, dot_leader, 0
191
258
  ensure
192
- @rendering_list = false
259
+ @rendering_list = false
260
+ @list_strip_period = false
261
+ @list_split_caption = false
262
+ @list_entry_indent = nil
263
+ @list_first_entry_margin = nil
193
264
  end
194
265
  end
195
266
 
@@ -241,16 +312,26 @@ module Asciidoctor
241
312
  if scope_node.equal?(doc)
242
313
  # ── Document-wide macro ────────────────────────────────────────
243
314
  # Remove from body and render in PDF front matter after the ToC.
244
- # Capture the parent section title first it becomes the list heading.
315
+ # Capture the parent section title (list heading) and any extra
316
+ # sibling blocks (admonitions, paragraphs, etc.) so they are
317
+ # rendered in the front matter alongside the list rather than left
318
+ # as orphaned content in the body.
245
319
  section_title = (parent.context == :section) ? parent.title : nil
320
+ section_body = parent.context == :section ?
321
+ parent.blocks.reject { |b| b.object_id == block.object_id } : []
322
+ # Page-break blocks (<<<) must be deleted from the section but must NOT
323
+ # be rendered in front matter — they only existed to separate list sections
324
+ # from body chapters.
325
+ extra_blocks = section_body.reject { |b| b.context == :page_break }
326
+
246
327
  parent.blocks.delete(block)
247
- if parent.context == :section && parent.blocks.empty?
248
- parent.parent&.blocks&.delete(parent)
249
- end
328
+ section_body.each { |b| parent.blocks.delete(b) }
329
+ parent.parent&.blocks&.delete(parent) if parent.context == :section && parent.blocks.empty?
250
330
 
251
331
  next if entries.empty?
252
332
 
253
- config_with_uuid = config.merge(uuid: uuid, section_title: section_title, scope_node: scope_node)
333
+ config_with_uuid = config.merge(uuid: uuid, section_title: section_title,
334
+ section_blocks: extra_blocks, scope_node: scope_node)
254
335
  @list_configs_ordered << config_with_uuid
255
336
  @list_extents[uuid] = allocate_list(doc, config_with_uuid, num_levels, start_cursor, break_after)
256
337
  else
@@ -262,6 +343,12 @@ module Asciidoctor
262
343
  @inline_list_configs[uuid] = config_with_uuid
263
344
  end
264
345
  end
346
+
347
+ # Strip leading page-break blocks left over after list sections were
348
+ # removed. A `<<<` placed between the last list section and the first
349
+ # chapter now sits orphaned at the document level; without this cleanup
350
+ # it renders as a blank page before Chapter 1.
351
+ doc.blocks.shift while doc.blocks.first&.context == :page_break
265
352
  end
266
353
 
267
354
  # -----------------------------------------------------------------------
@@ -271,17 +358,12 @@ module Asciidoctor
271
358
  # that many pages. Follows the same pattern as rhrev's
272
359
  # allocate_revision_history_extent and asciidoctor-pdf-lofte's allocate_lof.
273
360
  def allocate_list(doc, config, num_levels, _start_cursor, break_after)
274
- to_page = nil
275
- extent = dry_run onto: self do
276
- to_page = ink_list(doc, config, nil, num_levels, 0).last
361
+ extent = dry_run onto: self do
362
+ ink_list(doc, config, nil, num_levels, 0)
277
363
  theme_margin :block, :bottom unless break_after
278
364
  end
279
365
 
280
- if to_page && to_page > extent.to.page
281
- extent.to.page = to_page
282
- extent.to.cursor = bounds.height
283
- end
284
-
366
+ page_before = page_number
285
367
  if break_after
286
368
  extent.each_page { start_new_page }
287
369
  else
@@ -333,6 +415,7 @@ module Asciidoctor
333
415
  def ink_list_toc_level(entries, num_levels, dot_leader, num_front_matter_pages)
334
416
  toc_font_info = theme_font(:toc) { { font: font, size: @font_size } }
335
417
  hanging_indent = @theme.toc_hanging_indent
418
+ is_first_entry = true
336
419
 
337
420
  entries.each do |entry|
338
421
  next if (num_levels_for_entry = (entry.attr 'toclevels', num_levels).to_i) <
@@ -346,26 +429,69 @@ module Asciidoctor
346
429
 
347
430
  # For captioned blocks (image, table, example, listing, custom) prefer
348
431
  # captioned_title, e.g. "Figure 1. My Diagram" over "My Diagram".
432
+ # When strip_period is active, rebuild from caption without the trailing
433
+ # period, e.g. "Figure 1 My Diagram".
349
434
  if entry.context != :section && entry.respond_to?(:captioned_title)
350
- captioned = entry.captioned_title
351
- entry_title = captioned unless captioned.nil_or_empty?
435
+ if @list_strip_period && entry.caption
436
+ stripped_caption = entry.caption.sub(/\.\s*\z/, ' ')
437
+ entry_title = "#{stripped_caption}#{entry.title}"
438
+ else
439
+ captioned = entry.captioned_title
440
+ entry_title = captioned unless captioned.nil_or_empty?
441
+ end
352
442
  end
353
443
 
354
444
  next if entry_title.nil_or_empty?
355
445
 
446
+ # Apply first_entry_margin before the very first entry
447
+ if is_first_entry && @list_first_entry_margin
448
+ move_down @list_first_entry_margin
449
+ is_first_entry = false
450
+ else
451
+ is_first_entry = false
452
+ end
453
+
454
+ # Determine if we should split signifier from title
455
+ split_parts = nil
456
+ if @list_split_caption && entry.context != :section && entry.caption
457
+ parts = entry_title.split('. ', 2)
458
+ split_parts = { signifier: parts[0], title: parts[1] } if parts.length == 2
459
+ end
460
+
356
461
  theme_font :toc, level: entry_level do
357
- entry_title = transform_text entry_title, @text_transform if @text_transform
358
462
  pgnum_label_placeholder_width = rendered_width_of_string '0' * @toc_max_pagenum_digits
359
463
 
464
+ # Resolve effective entry indent: macro override > theme > default
465
+ effective_indent = @list_entry_indent || 0
466
+
467
+ if split_parts
468
+ split_indent = effective_indent > 0 ? effective_indent : (bounds.width * 0.115)
469
+ split_parts[:signifier] = transform_text(split_parts[:signifier], @text_transform) if @text_transform
470
+ split_parts[:title] = transform_text(split_parts[:title], @text_transform) if @text_transform
471
+ else
472
+ entry_title = transform_text entry_title, @text_transform if @text_transform
473
+ end
474
+
360
475
  if scratch?
361
476
  # Dry-run pass: measure space only
362
- indent 0, pgnum_label_placeholder_width do
363
- ink_prose entry_title,
364
- anchor: true,
365
- normalize: false,
366
- hanging_indent: hanging_indent,
367
- normalize_line_height: true,
368
- margin: 0
477
+ if split_parts
478
+ indent split_indent, pgnum_label_placeholder_width do
479
+ ink_prose split_parts[:title],
480
+ anchor: true,
481
+ normalize: false,
482
+ hanging_indent: hanging_indent,
483
+ normalize_line_height: true,
484
+ margin: 0
485
+ end
486
+ else
487
+ indent effective_indent, pgnum_label_placeholder_width do
488
+ ink_prose entry_title,
489
+ anchor: true,
490
+ normalize: false,
491
+ hanging_indent: hanging_indent,
492
+ normalize_line_height: true,
493
+ margin: 0
494
+ end
369
495
  end
370
496
  else
371
497
  # Real render pass: resolve page number and draw dot leader
@@ -392,27 +518,65 @@ module Asciidoctor
392
518
 
393
519
  entry_title_inherited = (apply_text_decoration ::Set.new, :toc, entry_level)
394
520
  .merge(anchor: entry_anchor, color: @font_color)
395
- entry_title_fragments = text_formatter.format entry_title,
396
- inherited: entry_title_inherited
397
521
  line_metrics = calc_line_metrics @base_line_height
398
522
 
399
- indent 0, pgnum_label_placeholder_width do
400
- fragment_positions = entry_title_fragments.map do |fragment|
401
- fp = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new
402
- (fragment[:callback] ||= []) << fp
403
- fp
404
- end
405
- typeset_formatted_text entry_title_fragments, line_metrics,
406
- hanging_indent: hanging_indent,
523
+ if split_parts
524
+ # ── Split caption: signifier at left, title indented ──
525
+ split_indent = effective_indent > 0 ? effective_indent : (bounds.width * 0.115)
526
+
527
+ # Phase 1: Render signifier (e.g. "Table 1") at normal position
528
+ signifier_fragments = text_formatter.format split_parts[:signifier],
529
+ inherited: entry_title_inherited
530
+ typeset_formatted_text signifier_fragments, line_metrics,
407
531
  normalize_line_height: true
408
- last_fp = fragment_positions.select(&:page_number).last
409
- break unless last_fp
410
-
411
- start_dots = last_fp.right + hanging_indent
412
- last_frag_cursor = last_fp.top + line_metrics.padding_top
413
- if last_fp.page_number > start_page_number ||
414
- (start_cursor - last_frag_cursor) > line_metrics.height
415
- start_cursor = last_frag_cursor
532
+ move_cursor_to start_cursor
533
+
534
+ # Phase 2: Render title indented, with fragment tracking for dot leaders
535
+ indent split_indent, pgnum_label_placeholder_width do
536
+ title_fragments = text_formatter.format split_parts[:title],
537
+ inherited: entry_title_inherited
538
+ fragment_positions = title_fragments.map do |fragment|
539
+ fp = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new
540
+ (fragment[:callback] ||= []) << fp
541
+ fp
542
+ end
543
+ typeset_formatted_text title_fragments, line_metrics,
544
+ hanging_indent: hanging_indent,
545
+ normalize_line_height: true
546
+ last_fp = fragment_positions.select(&:page_number).last
547
+ break unless last_fp
548
+
549
+ # Adjust start_dots to full-bounds coordinates
550
+ start_dots = last_fp.right + hanging_indent + split_indent
551
+ last_frag_cursor = last_fp.top + line_metrics.padding_top
552
+ if last_fp.page_number > start_page_number ||
553
+ (start_cursor - last_frag_cursor) > line_metrics.height
554
+ start_cursor = last_frag_cursor
555
+ end
556
+ end
557
+ else
558
+ # ── Normal (non-split) rendering ──
559
+ entry_title_fragments = text_formatter.format entry_title,
560
+ inherited: entry_title_inherited
561
+
562
+ indent effective_indent, pgnum_label_placeholder_width do
563
+ fragment_positions = entry_title_fragments.map do |fragment|
564
+ fp = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new
565
+ (fragment[:callback] ||= []) << fp
566
+ fp
567
+ end
568
+ typeset_formatted_text entry_title_fragments, line_metrics,
569
+ hanging_indent: hanging_indent,
570
+ normalize_line_height: true
571
+ last_fp = fragment_positions.select(&:page_number).last
572
+ break unless last_fp
573
+
574
+ start_dots = last_fp.right + hanging_indent + effective_indent
575
+ last_frag_cursor = last_fp.top + line_metrics.padding_top
576
+ if last_fp.page_number > start_page_number ||
577
+ (start_cursor - last_frag_cursor) > line_metrics.height
578
+ start_cursor = last_frag_cursor
579
+ end
416
580
  end
417
581
  end
418
582
 
@@ -40,14 +40,33 @@ module Asciidoctor
40
40
  end
41
41
  end
42
42
 
43
+ # Render any extra blocks from the original section (admonitions, paragraphs,
44
+ # etc.) that were captured alongside the list-of:: macro. They appear after
45
+ # the heading but before the dot-leader entry rows. Running during both the
46
+ # dry-run and real pass ensures the allocated space is correct.
47
+ (config[:section_blocks] || []).each { |b| convert b }
48
+
43
49
  unless num_levels < 0 || entries.empty?
44
50
  dot_leader = build_dot_leader_config(num_levels)
45
51
  theme_margin :toc, :top
52
+
53
+ caption_style = resolve_caption_style(config)
54
+ entry_indent = config[:entry_indent] || @theme.asciidoctor_lists_extended_entry_indent
55
+ first_entry_margin = config[:first_entry_margin] || @theme.asciidoctor_lists_extended_first_entry_margin
56
+
46
57
  begin
47
- @rendering_list = true
58
+ @rendering_list = true
59
+ @list_strip_period = (caption_style == 'strip')
60
+ @list_split_caption = (caption_style == 'split')
61
+ @list_entry_indent = entry_indent
62
+ @list_first_entry_margin = first_entry_margin
48
63
  ink_toc_level entries, num_levels, dot_leader, num_front_matter_pages
49
64
  ensure
50
- @rendering_list = false
65
+ @rendering_list = false
66
+ @list_strip_period = false
67
+ @list_split_caption = false
68
+ @list_entry_indent = nil
69
+ @list_first_entry_margin = nil
51
70
  end
52
71
  end
53
72
  end
@@ -58,7 +77,7 @@ module Asciidoctor
58
77
  def get_list_entries(scope_node, element_type)
59
78
  scope_node
60
79
  .find_by(traverse_documents: true, context: element_type.to_sym)
61
- .select { |e| e.caption || e.title }
80
+ .select { |e| (e.caption || e.title) && e.style != 'discrete' }
62
81
  end
63
82
 
64
83
  # Insert a virtual Section node into the document AST at the correct position
@@ -78,7 +97,10 @@ module Asciidoctor
78
97
  toc_level = doc.sections[0]&.level || 1
79
98
  base_idx = 0
80
99
  end
81
- toc_dest = toc_node.attr 'pdf-destination'
100
+ # NOTE: do NOT use toc_node.attr('pdf-destination') — that is the ToC
101
+ # page's own destination, which would make every list bookmark navigate
102
+ # back to the ToC page instead of the actual list page.
103
+ toc_dest = dest_top list_page_nums.first
82
104
  else
83
105
  grandparent_section = doc
84
106
  toc_level = doc.sections[0]&.level || 1
@@ -90,12 +112,37 @@ module Asciidoctor
90
112
  attributes: { 'pdf-destination' => toc_dest }
91
113
  list_section.title = list_title
92
114
  list_section.id = list_id
115
+ list_section.set_attr 'pdf-page-start', list_page_nums.first
93
116
  list_section.set_attr 'list-exclude-from-toc', '' if exclude_from_toc
94
117
  list_section.set_attr 'list-exclude-from-outline', '' if exclude_from_outline
95
118
  grandparent_section.blocks.insert base_idx + insert_idx, list_section
96
119
  list_section
97
120
  end
98
121
 
122
+ # Resolve the effective caption_style for a list config.
123
+ # Priority: macro attribute > theme key > built-in default ('default').
124
+ def resolve_caption_style(config)
125
+ return 'split' if config[:split_caption]
126
+ return 'strip' if config[:strip_period]
127
+ @theme.asciidoctor_lists_extended_caption_style || 'default'
128
+ end
129
+
130
+ # Resolve whether a list should be excluded from the ToC.
131
+ # Priority: macro flag > per-element theme key > false.
132
+ def resolve_exclude_from_toc(config)
133
+ return true if config[:exclude_from_toc]
134
+ element_key = :"asciidoctor_lists_extended_#{config[:element].tr('-', '_')}_exclude_from_toc"
135
+ @theme.respond_to?(element_key) ? @theme.send(element_key) : false
136
+ end
137
+
138
+ # Resolve whether a list should be excluded from the PDF outline.
139
+ # Priority: macro flag > per-element theme key > false.
140
+ def resolve_exclude_from_outline(config)
141
+ return true if config[:exclude_from_outline]
142
+ element_key = :"asciidoctor_lists_extended_#{config[:element].tr('-', '_')}_exclude_from_outline"
143
+ @theme.respond_to?(element_key) ? @theme.send(element_key) : false
144
+ end
145
+
99
146
  private
100
147
 
101
148
  # Build the dot-leader configuration hash expected by ink_toc_level.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-lists-extended
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 白一百 baiyibai