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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fef48386d4e8e43dc4b346f18f0eacc159e4660ae72a37b7d0e309bcee7f1b5
|
|
4
|
+
data.tar.gz: 794aa84d51d840cfcf72deea61a208030070c9efc0779d1ee70c4bdf2997f655
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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 (~
|
|
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
|
|
40
|
-
%(xref:#{element.id}[#{
|
|
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
|
|
46
|
-
%(xref:#{element.id}[#{
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
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,
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|