asciidoctor-pdf 2.0.0.alpha.1 → 2.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +94 -1
  3. data/README.adoc +32 -14
  4. data/data/fonts/ABOUT-mplus1p-subset +1 -0
  5. data/data/fonts/ABOUT-notosans-subset +1 -0
  6. data/data/fonts/ABOUT-notoserif-subset +1 -0
  7. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  8. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  9. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  10. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  11. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  12. data/data/fonts/notosans-bold-subset.ttf +0 -0
  13. data/data/fonts/notosans-bold_italic-subset.ttf +0 -0
  14. data/data/fonts/notosans-italic-subset.ttf +0 -0
  15. data/data/fonts/notosans-regular-subset.ttf +0 -0
  16. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  17. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  18. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  19. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  20. data/data/themes/base-theme.yml +11 -20
  21. data/data/themes/default-theme.yml +8 -7
  22. data/docs/theming-guide.adoc +11 -10
  23. data/lib/asciidoctor/pdf/converter.rb +445 -305
  24. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +4 -0
  25. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +8 -0
  26. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +117 -38
  27. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +5 -0
  28. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/protect_bottom_gutter.rb +13 -0
  29. data/lib/asciidoctor/pdf/ext/prawn/images.rb +6 -2
  30. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +1 -1
  31. data/lib/asciidoctor/pdf/ext/prawn.rb +1 -0
  32. data/lib/asciidoctor/pdf/formatted_text/transform.rb +7 -2
  33. data/lib/asciidoctor/pdf/nogmagick.rb +6 -0
  34. data/lib/asciidoctor/pdf/nopngmagick.rb +3 -0
  35. data/lib/asciidoctor/pdf/optimizer.rb +12 -5
  36. data/lib/asciidoctor/pdf/text_transformer.rb +14 -0
  37. data/lib/asciidoctor/pdf/theme_loader.rb +19 -3
  38. data/lib/asciidoctor/pdf/version.rb +1 -1
  39. metadata +5 -2
@@ -139,6 +139,7 @@ module Asciidoctor
139
139
  # NOTE: enabling data-uri forces Asciidoctor Diagram to produce absolute image paths
140
140
  doc.attributes['data-uri'] = (doc.instance_variable_get :@attribute_overrides)['data-uri'] = ''
141
141
  end
142
+ @label = :primary
142
143
  @initial_instance_variables = [:@initial_instance_variables] + instance_variables
143
144
  end
144
145
 
@@ -164,8 +165,11 @@ module Asciidoctor
164
165
  if node.blocks?
165
166
  node.content
166
167
  elsif node.content_model != :compound && (string = node.content)
167
- # TODO: this content could be cached on repeat invocations!
168
- layout_prose string, (opts.merge hyphenate: true, margin_bottom: 0)
168
+ prose_opts = opts.merge hyphenate: true, margin_bottom: 0
169
+ if (bottom_gutter = @bottom_gutters[-1][node])
170
+ prose_opts[:bottom_gutter] = bottom_gutter
171
+ end
172
+ ink_prose string, prose_opts
169
173
  end
170
174
  ensure
171
175
  node.document.instance_variable_set :@converter, prev_converter if prev_converter
@@ -178,8 +182,9 @@ module Asciidoctor
178
182
  def convert_document doc
179
183
  doc.promote_preface_block
180
184
  init_pdf doc
181
- # set default value for outline, pagenums, and show-link-uri if not otherwise set
185
+ # set default value for outline, outline-title, and pagenums attributes if not otherwise set
182
186
  doc.attributes['outline'] = '' unless (doc.attribute_locked? 'outline') || ((doc.instance_variable_get :@attributes_modified).include? 'outline')
187
+ doc.attributes['outline-title'] = '' unless (doc.attribute_locked? 'outline-title') || ((doc.instance_variable_get :@attributes_modified).include? 'outline-title')
183
188
  doc.attributes['pagenums'] = '' unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
184
189
  #assign_missing_section_ids doc
185
190
 
@@ -187,25 +192,22 @@ module Asciidoctor
187
192
 
188
193
  marked_page_number = page_number
189
194
  # NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF
190
- layout_cover_page doc, :front
195
+ ink_cover_page doc, :front
191
196
  has_front_cover = page_number > marked_page_number
192
-
193
- if (use_title_page = doc.doctype == 'book' || (doc.attr? 'title-page'))
194
- layout_title_page doc
195
- has_title_page = page_number == (has_front_cover ? 2 : 1)
196
- end
197
+ has_title_page = ink_title_page doc if (title_page_on = doc.doctype == 'book' || (doc.attr? 'title-page'))
197
198
 
198
199
  @page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress' && page_number == 0
199
200
 
200
201
  start_new_page unless page&.empty? # rubocop:disable Lint/SafeNavigationWithEmpty
201
202
 
202
- # NOTE: font must be set before content is written to the main or scratch document
203
+ # NOTE: the base font must be set before any content is written to the main or scratch document
204
+ # this method is called inside ink_title_page if the title page is active
203
205
  font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style unless has_title_page
204
206
 
205
- unless use_title_page
207
+ unless title_page_on
206
208
  body_start_page_number = page_number
207
209
  theme_font :heading, level: 1 do
208
- layout_general_heading doc, doc.doctitle, align: (@theme.heading_h1_align&.to_sym || :center), level: 1, role: :doctitle
210
+ ink_general_heading doc, doc.doctitle, align: (@theme.heading_h1_text_align&.to_sym || :center), level: 1, role: :doctitle
209
211
  end if doc.header? && !doc.notitle
210
212
  end
211
213
 
@@ -216,14 +218,14 @@ module Asciidoctor
216
218
  if (insert_toc = (doc.attr? 'toc') && !((toc_placement = doc.attr 'toc-placement') == 'macro' || toc_placement == 'preamble') && doc.sections?)
217
219
  start_new_page if @ppbook && verso_page?
218
220
  add_dest_for_block doc, id: 'toc', y: (at_page_top? ? page_height : nil)
219
- allocate_toc doc, toc_num_levels, cursor, use_title_page
221
+ @toc_extent = allocate_toc doc, toc_num_levels, cursor, title_page_on
220
222
  else
221
223
  @toc_extent = nil
222
224
  end
223
225
 
224
226
  start_new_page if @ppbook && verso_page? && !(((next_block = doc.blocks[0])&.context == :preamble ? next_block.blocks[0] : next_block)&.option? 'nonfacing')
225
227
 
226
- if use_title_page
228
+ if title_page_on
227
229
  zero_page_offset = has_front_cover ? 1 : 0
228
230
  first_page_offset = has_title_page ? zero_page_offset.next : zero_page_offset
229
231
  body_offset = (body_start_page_number = page_number) - 1
@@ -301,26 +303,26 @@ module Asciidoctor
301
303
  traverse doc
302
304
 
303
305
  # NOTE: for a book, these are leftover footnotes; for an article this is everything
304
- outdent_section { layout_footnotes doc }
306
+ outdent_section { ink_footnotes doc }
305
307
 
306
- if @toc_extent
307
- if use_title_page && !insert_toc
308
- num_front_matter_pages[0] = @toc_extent.to.page if @theme.running_content_start_at == 'after-toc'
309
- num_front_matter_pages[1] = @toc_extent.to.page if @theme.page_numbering_start_at == 'after-toc'
308
+ if (toc_extent = @toc_extent)
309
+ if title_page_on && !insert_toc
310
+ num_front_matter_pages[0] = toc_extent.to.page if @theme.running_content_start_at == 'after-toc'
311
+ num_front_matter_pages[1] = toc_extent.to.page if @theme.page_numbering_start_at == 'after-toc'
310
312
  end
311
- toc_page_nums = layout_toc doc, toc_num_levels, @toc_extent.from.page, @toc_extent.from.cursor, num_front_matter_pages[1]
313
+ toc_page_nums = ink_toc doc, toc_num_levels, toc_extent.from.page, toc_extent.from.cursor, num_front_matter_pages[1]
312
314
  else
313
315
  toc_page_nums = []
314
316
  end
315
317
 
316
318
  # NOTE: delete orphaned page (a page was created but there was no additional content)
317
319
  # QUESTION: should we delete page if document is empty? (leaving no pages?)
318
- delete_page if page_count > 1 && page.empty?
320
+ delete_current_page if page_count > 1 && page.empty?
319
321
  end
320
322
 
321
323
  unless page_count < body_start_page_number
322
- layout_running_content :header, doc, num_front_matter_pages, body_start_page_number unless doc.noheader || @theme.header_height.to_f == 0 # rubocop:disable Lint/FloatComparison
323
- layout_running_content :footer, doc, num_front_matter_pages, body_start_page_number unless doc.nofooter || @theme.footer_height.to_f == 0 # rubocop:disable Lint/FloatComparison
324
+ ink_running_content :header, doc, num_front_matter_pages, body_start_page_number unless doc.noheader || @theme.header_height.to_f == 0 # rubocop:disable Lint/FloatComparison
325
+ ink_running_content :footer, doc, num_front_matter_pages, body_start_page_number unless doc.nofooter || @theme.footer_height.to_f == 0 # rubocop:disable Lint/FloatComparison
324
326
  end
325
327
 
326
328
  add_outline doc, (doc.attr 'outlinelevels', toc_num_levels), toc_page_nums, num_front_matter_pages[1], has_front_cover
@@ -337,7 +339,7 @@ module Asciidoctor
337
339
  catalog.data[:ViewerPreferences] = { DisplayDocTitle: true }
338
340
 
339
341
  stamp_foreground_image doc, has_front_cover
340
- layout_cover_page doc, :back
342
+ ink_cover_page doc, :back
341
343
  add_dest_for_top doc
342
344
  nil
343
345
  end
@@ -402,7 +404,7 @@ module Asciidoctor
402
404
  @font_scale = 1
403
405
  @font_color = theme.base_font_color
404
406
  @text_decoration_width = theme.base_text_decoration_width
405
- @base_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_align
407
+ @base_text_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_text_align
406
408
  @base_line_height = theme.base_line_height
407
409
  @cjk_line_breaks = doc.attr? 'scripts', 'cjk'
408
410
  if (hyphen_lang = doc.attr 'hyphens') &&
@@ -415,6 +417,7 @@ module Asciidoctor
415
417
  @text_transform = nil
416
418
  @list_numerals = []
417
419
  @list_bullets = []
420
+ @bottom_gutters = [{}]
418
421
  @rendered_footnotes = []
419
422
  @conum_glyphs = ConumSets[@theme.conum_glyphs || 'circled'] || (@theme.conum_glyphs.split ',').map do |r|
420
423
  from, to = r.lstrip.split '-', 2
@@ -460,7 +463,6 @@ module Asciidoctor
460
463
  end
461
464
 
462
465
  def prepare_theme theme
463
- theme.base_border_width || 0
464
466
  theme.base_font_color ||= '000000'
465
467
  theme.base_font_size ||= 12
466
468
  theme.base_font_style = theme.base_font_style&.to_sym || :normal
@@ -477,6 +479,9 @@ module Asciidoctor
477
479
  theme.list_item_spacing ||= 0
478
480
  theme.description_list_term_spacing ||= 0
479
481
  theme.description_list_description_indent ||= 0
482
+ theme.table_border_color ||= (theme.base_border_color || '000000')
483
+ theme.table_border_width ||= 0.5
484
+ theme.thematic_break_border_color ||= (theme.base_border_color || '000000')
480
485
  theme.image_border_width ||= 0
481
486
  theme.code_linenum_font_color ||= '999999'
482
487
  theme.callout_list_margin_top_after_code ||= 0
@@ -661,22 +666,23 @@ module Asciidoctor
661
666
  title = %(#{title}\n<em class="subtitle">#{subtitle}</em>)
662
667
  end
663
668
  hlevel = sect.level + 1
664
- align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
669
+ align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
665
670
  chapterlike = !(part = sectname == 'part') && (sectname == 'chapter' || (sect.document.doctype == 'book' && sect.level == 1))
666
- hopts = { align: align, level: hlevel, outdent: !(part || chapterlike) }
671
+ hidden = sect.option? 'notitle'
672
+ hopts = { align: align, level: hlevel, part: part, chapterlike: chapterlike, outdent: !(part || chapterlike) }
667
673
  if part
668
674
  unless @theme.heading_part_break_before == 'auto'
669
675
  start_new = true
670
- theme_font(:heading, level: hlevel) { start_new_part sect }
676
+ start_new_part sect
671
677
  end
672
678
  elsif chapterlike
673
679
  if @theme.heading_chapter_break_before != 'auto' ||
674
680
  (@theme.heading_part_break_after == 'always' && sect == sect.parent.sections[0])
675
681
  start_new = true
676
- theme_font(:heading, level: hlevel) { start_new_chapter sect }
682
+ start_new_chapter sect
677
683
  end
678
684
  end
679
- arrange_section sect, title, hopts unless start_new || at_page_top?
685
+ arrange_section sect, title, hopts unless hidden || start_new || at_page_top?
680
686
  # QUESTION: should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
681
687
  sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
682
688
  # QUESTION: should we just assign the section this generated id?
@@ -685,20 +691,20 @@ module Asciidoctor
685
691
  add_dest_for_block sect, id: sect_anchor, y: (at_page_top? ? page_height : nil)
686
692
  theme_font :heading, level: hlevel do
687
693
  if part
688
- layout_part_title sect, title, hopts
694
+ ink_part_title sect, title, hopts
689
695
  elsif chapterlike
690
- layout_chapter_title sect, title, hopts unless sect.special && (sect.option? 'untitled')
696
+ ink_chapter_title sect, title, hopts
691
697
  else
692
- layout_general_heading sect, title, hopts
698
+ ink_general_heading sect, title, hopts
693
699
  end
694
- end
700
+ end unless hidden
695
701
 
696
702
  if index_section
697
703
  outdent_section { convert_index_section sect }
698
704
  else
699
705
  traverse sect
700
706
  end
701
- outdent_section { layout_footnotes sect } if chapterlike
707
+ outdent_section { ink_footnotes sect } if chapterlike
702
708
  sect.set_attr 'pdf-page-end', page_number
703
709
  end
704
710
 
@@ -719,7 +725,7 @@ module Asciidoctor
719
725
  end
720
726
 
721
727
  # QUESTION: if a footnote ref appears in a separate chapter, should the footnote def be duplicated?
722
- def layout_footnotes node
728
+ def ink_footnotes node
723
729
  return if (fns = (doc = node.document).footnotes - @rendered_footnotes).empty?
724
730
  theme_margin :block, :bottom if node.context == :document || node == node.document.blocks[-1]
725
731
  theme_margin :footnotes, :top
@@ -728,7 +734,7 @@ module Asciidoctor
728
734
  move_down delta
729
735
  end
730
736
  theme_font :footnotes do
731
- (title = doc.attr 'footnotes-title') && (layout_caption title, category: :footnotes)
737
+ (title = doc.attr 'footnotes-title') && (ink_caption title, category: :footnotes)
732
738
  item_spacing = @theme.footnotes_item_spacing
733
739
  index_offset = @rendered_footnotes.length
734
740
  sect_xreftext = node.context == :section && (node.xreftext node.document.attr 'xrefstyle')
@@ -738,7 +744,7 @@ module Asciidoctor
738
744
  fn.singleton_class.send :attr_accessor, :label unless fn.respond_to? :label=
739
745
  fn.label = %(#{label} - #{sect_xreftext})
740
746
  end
741
- layout_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
747
+ ink_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
742
748
  end
743
749
  @rendered_footnotes += fns if extent
744
750
  end
@@ -750,11 +756,11 @@ module Asciidoctor
750
756
  add_dest_for_block node if node.id
751
757
  hlevel = node.level.next
752
758
  unless (align = resolve_alignment_from_role node.roles)
753
- align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
759
+ align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
754
760
  end
755
761
  # QUESTION: should we decouple styles from section titles?
756
762
  theme_font :heading, level: hlevel do
757
- layout_general_heading node, node.title, align: align, level: hlevel, outdent: (node.parent.context == :section)
763
+ ink_general_heading node, node.title, align: align, level: hlevel, outdent: (node.parent.context == :section)
758
764
  end
759
765
  end
760
766
 
@@ -763,10 +769,10 @@ module Asciidoctor
763
769
  outdent_section do
764
770
  pad_box @theme.abstract_padding do
765
771
  theme_font :abstract_title do
766
- layout_prose node.title, align: (@theme.abstract_title_align || @base_align).to_sym, margin_top: @theme.heading_margin_top, margin_bottom: @theme.heading_margin_bottom, line_height: (@theme.heading_line_height || @theme.base_line_height)
772
+ ink_prose node.title, align: (@theme.abstract_title_text_align || @base_text_align).to_sym, margin_top: @theme.heading_margin_top, margin_bottom: @theme.heading_margin_bottom, line_height: (@theme.heading_line_height || @theme.base_line_height)
767
773
  end if node.title?
768
774
  theme_font :abstract do
769
- prose_opts = { align: (@theme.abstract_align || @base_align).to_sym, hyphenate: true }
775
+ prose_opts = { align: (@theme.abstract_text_align || @base_text_align).to_sym, hyphenate: true }
770
776
  if (text_indent = @theme.prose_text_indent) > 0
771
777
  prose_opts[:indent_paragraphs] = text_indent
772
778
  end
@@ -785,7 +791,7 @@ module Asciidoctor
785
791
  if child.context == :paragraph
786
792
  child.document.playback_attributes child.attributes
787
793
  prose_opts[:margin_bottom] = 0 if child == last_block
788
- layout_prose child.content, ((align = resolve_alignment_from_role child.roles) ? (prose_opts.merge align: align) : prose_opts.dup)
794
+ ink_prose child.content, ((align = resolve_alignment_from_role child.roles) ? (prose_opts.merge align: align) : prose_opts.dup)
789
795
  prose_opts.delete :first_line_options
790
796
  prose_opts.delete :margin_bottom
791
797
  else
@@ -797,7 +803,7 @@ module Asciidoctor
797
803
  if (align = resolve_alignment_from_role node.roles)
798
804
  prose_opts[:align] = align
799
805
  end
800
- layout_prose string, (prose_opts.merge margin_bottom: 0)
806
+ ink_prose string, (prose_opts.merge margin_bottom: 0)
801
807
  end
802
808
  end
803
809
  end
@@ -809,7 +815,7 @@ module Asciidoctor
809
815
  def convert_preamble node
810
816
  # FIXME: core should not be promoting paragraph to preamble if there are no sections
811
817
  if node.blocks? && (first_block = node.blocks[0]).context == :paragraph && node.document.sections? && !first_block.role?
812
- first_block.add_role 'lead'
818
+ first_block.role = 'lead'
813
819
  end
814
820
  traverse node
815
821
  theme_margin :block, :bottom, (next_enclosed_block node)
@@ -832,13 +838,17 @@ module Asciidoctor
832
838
 
833
839
  # TODO: check if we're within one line of the bottom of the page
834
840
  # and advance to the next page if so (similar to logic for section titles)
835
- layout_caption node, labeled: false if node.title?
841
+ ink_caption node, labeled: false if node.title?
842
+
843
+ if (bottom_gutter = @bottom_gutters[-1][node])
844
+ prose_opts[:bottom_gutter] = bottom_gutter
845
+ end
836
846
 
837
847
  if roles.empty?
838
- layout_prose node.content, prose_opts
848
+ ink_prose node.content, prose_opts
839
849
  else
840
850
  theme_font_cascade (roles.map {|role| %(role_#{role}).to_sym }) do
841
- layout_prose node.content, prose_opts
851
+ ink_prose node.content, prose_opts
842
852
  end
843
853
  end
844
854
 
@@ -852,7 +862,7 @@ module Asciidoctor
852
862
 
853
863
  def convert_admonition node
854
864
  type = node.attr 'name'
855
- label_align = @theme.admonition_label_align&.to_sym || :center
865
+ label_align = @theme.admonition_label_text_align&.to_sym || :center
856
866
  # TODO: allow vertical_align to be a number
857
867
  if (label_valign = @theme.admonition_label_vertical_align&.to_sym || :middle) == :middle
858
868
  label_valign = :center
@@ -861,18 +871,21 @@ module Asciidoctor
861
871
  label_min_width = label_min_width.to_f
862
872
  end
863
873
  if (doc = node.document).attr? 'icons'
864
- if (doc.attr 'icons') == 'font' && !(node.attr? 'icon')
874
+ if !(has_icon = node.attr? 'icon') && (doc.attr 'icons') == 'font'
865
875
  icons = 'font'
866
876
  label_text = type.to_sym
867
877
  icon_data = admonition_icon_data label_text
868
878
  icon_size = icon_data[:size] || 24
869
879
  label_width = label_min_width || (icon_size * 1.5)
870
- elsif (icon_path = resolve_icon_image_path node, type) && (::File.readable? icon_path)
880
+ elsif (icon_path = has_icon || !(icon_path = (@theme[%(admonition_icon_#{type})] || {})[:image]) ?
881
+ (resolve_icon_image_path node, type) :
882
+ (ThemeLoader.resolve_theme_asset (apply_subs_discretely doc, icon_path, subs: [:attributes]), @themesdir)) &&
883
+ (::File.readable? icon_path)
871
884
  icons = true
872
885
  # TODO: introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
873
886
  label_width = label_min_width || 36.0
874
887
  else
875
- log :warn, %(admonition icon not found or not readable: #{icon_path || (resolve_icon_image_path node, type, false)})
888
+ log :warn, %(admonition icon image#{has_icon ? '' : ' for ' + type.upcase} not found or not readable: #{icon_path || (resolve_icon_image_path node, type, false)})
876
889
  end
877
890
  end
878
891
  unless icons
@@ -883,20 +896,16 @@ module Asciidoctor
883
896
  label_width = label_min_width if label_min_width && label_min_width > label_width
884
897
  end
885
898
  end
886
- unless ::Array === (cpad = @theme.admonition_padding || 0)
887
- cpad = ::Array.new 4, cpad
888
- end
889
- unless ::Array === (lpad = @theme.admonition_label_padding || cpad)
890
- lpad = ::Array.new 4, lpad
891
- end
899
+ cpad = expand_padding_value @theme.admonition_padding
900
+ lpad = (lpad = @theme.admonition_label_padding) ? (expand_padding_value lpad) : cpad
892
901
  arrange_block node do |extent|
893
902
  add_dest_for_block node if node.id
894
903
  theme_fill_and_stroke_block :admonition, extent if extent
895
904
  pad_box [0, cpad[1], 0, lpad[3]] do
896
905
  if extent
897
906
  label_height = extent.single_page_height || cursor
898
- if (rule_color = @theme.admonition_column_rule_color) &&
899
- (rule_width = @theme.admonition_column_rule_width || @theme.base_border_width) && rule_width > 0
907
+ if (rule_width = @theme.admonition_column_rule_width || 0) > 0 &&
908
+ (rule_color = @theme.admonition_column_rule_color || @theme.base_border_color)
900
909
  rule_style = @theme.admonition_column_rule_style&.to_sym || :solid
901
910
  float do
902
911
  extent.each_page do |first_page, last_page|
@@ -945,7 +954,7 @@ module Asciidoctor
945
954
  log :warn, %(problem encountered in image: #{icon_path}; #{icon_warning})
946
955
  end unless scratch?
947
956
  rescue
948
- log :warn, %(could not embed admonition icon: #{icon_path}; #{$!.message})
957
+ log :warn, %(could not embed admonition icon image: #{icon_path}; #{$!.message})
949
958
  icons = nil
950
959
  end
951
960
  else
@@ -959,7 +968,7 @@ module Asciidoctor
959
968
  end
960
969
  embed_image image_obj, image_info, width: icon_width, position: label_align, vposition: label_valign
961
970
  rescue
962
- log :warn, %(could not embed admonition icon: #{icon_path}; #{$!.message})
971
+ log :warn, %(could not embed admonition icon image: #{icon_path}; #{$!.message})
963
972
  icons = nil
964
973
  end
965
974
  end
@@ -987,7 +996,7 @@ module Asciidoctor
987
996
  end
988
997
  end
989
998
  @text_transform = nil # already applied to label
990
- layout_prose label_text,
999
+ ink_prose label_text,
991
1000
  align: label_align,
992
1001
  valign: label_valign,
993
1002
  line_height: 1,
@@ -1000,8 +1009,8 @@ module Asciidoctor
1000
1009
  end
1001
1010
  end
1002
1011
  end
1003
- pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]] do
1004
- layout_caption node, category: :admonition, labeled: false if node.title?
1012
+ pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]], node do
1013
+ ink_caption node, category: :admonition, labeled: false if node.title?
1005
1014
  theme_font :admonition do
1006
1015
  traverse node
1007
1016
  end
@@ -1018,7 +1027,7 @@ module Asciidoctor
1018
1027
  tare_first_page_content_stream do
1019
1028
  theme_fill_and_stroke_block :example, extent, caption_node: node
1020
1029
  end
1021
- pad_box @theme.example_padding do
1030
+ pad_box @theme.example_padding, node do
1022
1031
  theme_font :example do
1023
1032
  traverse node
1024
1033
  end
@@ -1035,13 +1044,13 @@ module Asciidoctor
1035
1044
  arrange_block node do
1036
1045
  add_dest_for_block node if id
1037
1046
  tare_first_page_content_stream do
1038
- node.context == :example ? (layout_caption %(\u25bc #{node.title})) : (layout_caption node, labeled: false)
1047
+ node.context == :example ? (ink_caption %(\u25bc #{node.title})) : (ink_caption node, labeled: false)
1039
1048
  end if has_title
1040
1049
  traverse node
1041
1050
  end
1042
1051
  else
1043
1052
  add_dest_for_block node if id
1044
- node.context == :example ? (layout_caption %(\u25bc #{node.title})) : (layout_caption node, labeled: false) if has_title
1053
+ node.context == :example ? (ink_caption %(\u25bc #{node.title})) : (ink_caption node, labeled: false) if has_title
1045
1054
  traverse node
1046
1055
  end
1047
1056
  end
@@ -1050,11 +1059,18 @@ module Asciidoctor
1050
1059
  category = node.context == :quote ? :quote : :verse
1051
1060
  # NOTE: b_width and b_left_width are mutually exclusive
1052
1061
  if (b_left_width = @theme[%(#{category}_border_left_width)]) && b_left_width > 0
1053
- b_color = @theme[%(#{category}_border_color)]
1062
+ b_color = @theme[%(#{category}_border_color)] || @theme.base_border_color
1054
1063
  else
1055
1064
  b_left_width = nil
1056
1065
  b_width = nil if (b_width = @theme[%(#{category}_border_width)]) == 0
1057
1066
  end
1067
+ if (attribution = (node.attr? 'attribution') && (node.attr 'attribution'))
1068
+ # NOTE: temporary workaround to allow bare & to be used without having to wrap value in single quotes
1069
+ attribution = escape_amp attribution if attribution.include? '&'
1070
+ if (citetitle = node.attr 'citetitle') && (citetitle.include? '&')
1071
+ citetitle = escape_amp citetitle
1072
+ end
1073
+ end
1058
1074
  arrange_block node do |extent|
1059
1075
  add_dest_for_block node if node.id
1060
1076
  tare_first_page_content_stream do
@@ -1072,27 +1088,25 @@ module Asciidoctor
1072
1088
  end
1073
1089
  end
1074
1090
  end
1075
- pad_box @theme[%(#{category}_padding)] do
1091
+ pad_box @theme[%(#{category}_padding)], (attribution ? nil : node) do
1076
1092
  theme_font category do
1077
1093
  if category == :quote
1078
1094
  traverse node
1079
1095
  else # :verse
1080
1096
  content = guard_indentation node.content
1081
- layout_prose content, normalize: false, align: :left, hyphenate: true, margin_bottom: 0
1097
+ ink_prose content,
1098
+ normalize: false,
1099
+ align: :left,
1100
+ hyphenate: true,
1101
+ margin_bottom: 0,
1102
+ bottom_gutter: (attribution ? nil : @bottom_gutters[-1][node])
1082
1103
  end
1083
1104
  end
1084
- if node.attr? 'attribution'
1105
+ if attribution
1085
1106
  margin_bottom @theme.block_margin_bottom
1086
1107
  theme_font %(#{category}_cite) do
1087
- # NOTE: temporary workaround to allow bare & to be used without having to wrap value in single quotes
1088
- attribution = node.attr 'attribution'
1089
- attribution = escape_amp attribution if attribution.include? '&'
1090
- attribution_parts = [attribution]
1091
- if (citetitle = node.attr 'citetitle')
1092
- citetitle = escape_amp citetitle if citetitle.include? '&'
1093
- attribution_parts << citetitle
1094
- end
1095
- layout_prose %(#{EmDash} #{attribution_parts.join ', '}), align: :left, normalize: false, margin_bottom: 0
1108
+ attribution_parts = citetitle ? [attribution, citetitle] : [attribution]
1109
+ ink_prose %(#{EmDash} #{attribution_parts.join ', '}), align: :left, normalize: false, margin_bottom: 0
1096
1110
  end
1097
1111
  end
1098
1112
  end
@@ -1107,10 +1121,10 @@ module Asciidoctor
1107
1121
  arrange_block node do |extent|
1108
1122
  add_dest_for_block node if node.id
1109
1123
  theme_fill_and_stroke_block :sidebar, extent if extent
1110
- pad_box @theme.sidebar_padding do
1124
+ pad_box @theme.sidebar_padding, node do
1111
1125
  theme_font :sidebar_title do
1112
1126
  # QUESTION: should we allow margins of sidebar title to be customized?
1113
- layout_prose node.title, align: (@theme.sidebar_title_align || @theme.heading_align || @base_align).to_sym, margin_bottom: @theme.heading_margin_bottom, line_height: (@theme.heading_line_height || @theme.base_line_height)
1127
+ ink_prose node.title, align: (@theme.sidebar_title_text_align || @theme.heading_text_align || @base_text_align).to_sym, margin_bottom: @theme.heading_margin_bottom, line_height: (@theme.heading_line_height || @theme.base_line_height)
1114
1128
  end if node.title?
1115
1129
  theme_font :sidebar do
1116
1130
  traverse node
@@ -1149,7 +1163,7 @@ module Asciidoctor
1149
1163
  marker_width = rendered_width_of_string %(#{marker = conum_glyph index}x)
1150
1164
  float do
1151
1165
  bounding_box [0, cursor], width: marker_width do
1152
- layout_prose marker, align: :center, inline_format: false, margin: 0
1166
+ ink_prose marker, align: :center, inline_format: false, margin: 0
1153
1167
  end
1154
1168
  end
1155
1169
  end
@@ -1233,7 +1247,7 @@ module Asciidoctor
1233
1247
  max_term_width += (term_padding[1] + term_padding[3])
1234
1248
  term_column_width = [max_term_width, bounds.width * 0.5].min
1235
1249
  table table_data, position: :left, cell_style: { border_width: 0 }, column_widths: [term_column_width] do
1236
- @pdf.layout_table_caption node if node.title?
1250
+ @pdf.ink_table_caption node if node.title?
1237
1251
  end
1238
1252
  theme_margin :prose, :bottom, (next_enclosed_block actual_node) #unless actual_node.nested?
1239
1253
  when 'qanda'
@@ -1243,7 +1257,7 @@ module Asciidoctor
1243
1257
  else
1244
1258
  # TODO: check if we're within one line of the bottom of the page
1245
1259
  # and advance to the next page if so (similar to logic for section titles)
1246
- layout_caption node, category: :description_list, labeled: false if node.title?
1260
+ ink_caption node, category: :description_list, labeled: false if node.title?
1247
1261
 
1248
1262
  term_spacing = @theme.description_list_term_spacing
1249
1263
  term_height = theme_font(:description_list_term) { height_of_typeset_text 'A' }
@@ -1255,8 +1269,8 @@ module Asciidoctor
1255
1269
  term_font_styles = nil
1256
1270
  end
1257
1271
  terms.each_with_index do |term, idx|
1258
- # QUESTION: should we pass down styles in other calls to layout_prose
1259
- layout_prose term.text, margin_top: (idx > 0 ? term_spacing : 0), margin_bottom: 0, align: :left, normalize_line_height: true, styles: term_font_styles
1272
+ # QUESTION: should we pass down styles in other calls to ink_prose
1273
+ ink_prose term.text, margin_top: (idx > 0 ? term_spacing : 0), margin_bottom: 0, align: :left, normalize_line_height: true, styles: term_font_styles
1260
1274
  end
1261
1275
  end
1262
1276
  indent @theme.description_list_description_indent do
@@ -1343,7 +1357,7 @@ module Asciidoctor
1343
1357
  def convert_list node
1344
1358
  # TODO: check if we're within one line of the bottom of the page
1345
1359
  # and advance to the next page if so (similar to logic for section titles)
1346
- layout_caption node, category: :list, labeled: false if node.title?
1360
+ ink_caption node, category: :list, labeled: false if node.title?
1347
1361
 
1348
1362
  opts = {}
1349
1363
  if (align = resolve_alignment_from_role node.roles)
@@ -1440,7 +1454,7 @@ module Asciidoctor
1440
1454
  float do
1441
1455
  advance_page if @media == 'prepress' && cursor < marker_height
1442
1456
  flow_bounding_box position: start_position, width: marker_width do
1443
- layout_prose marker,
1457
+ ink_prose marker,
1444
1458
  align: :right,
1445
1459
  character_spacing: -0.5,
1446
1460
  color: marker_style[:font_color],
@@ -1468,16 +1482,16 @@ module Asciidoctor
1468
1482
  def traverse_list_item node, list_type, opts = {}
1469
1483
  if list_type == :dlist # qanda
1470
1484
  terms, desc = node
1471
- terms.each {|term| layout_prose %(<em>#{term.text}</em>), (opts.merge margin_bottom: @theme.description_list_term_spacing) }
1485
+ terms.each {|term| ink_prose %(<em>#{term.text}</em>), (opts.merge margin_bottom: @theme.description_list_term_spacing) }
1472
1486
  if desc
1473
- layout_prose desc.text, (opts.merge hyphenate: true) if desc.text?
1487
+ ink_prose desc.text, (opts.merge hyphenate: true) if desc.text?
1474
1488
  traverse desc
1475
1489
  end
1476
1490
  else
1477
1491
  if (primary_text = node.text).nil_or_empty?
1478
- layout_prose DummyText, opts unless node.blocks?
1492
+ ink_prose DummyText, opts unless node.blocks?
1479
1493
  else
1480
- layout_prose primary_text, (opts.merge hyphenate: true)
1494
+ ink_prose primary_text, (opts.merge hyphenate: true)
1481
1495
  end
1482
1496
  traverse node
1483
1497
  end
@@ -1498,24 +1512,33 @@ module Asciidoctor
1498
1512
  elsif (image_path = resolve_image_path node, target, image_format, (opts.fetch :relative_to_imagesdir, true))
1499
1513
  if image_format == 'pdf'
1500
1514
  if ::File.readable? image_path
1501
- if (id = node.id)
1515
+ if (replace = page.empty?) && ((parent = node.parent).attr? 'pdf-page-start', page_number) && (parent.attr? 'pdf-anchor')
1516
+ replace_parent = parent
1517
+ end
1518
+ if (id = node.id) || replace_parent
1502
1519
  add_dest_block = proc do
1503
- node.set_attr 'pdf-destination', (node_dest = dest_top)
1504
- add_dest id, node_dest
1520
+ node_dest = dest_top
1521
+ if id
1522
+ node.set_attr 'pdf-destination', node_dest
1523
+ add_dest id, node_dest
1524
+ end
1525
+ if replace_parent
1526
+ replace_parent.set_attr 'pdf-destination', node_dest
1527
+ add_dest (replace_parent.attr 'pdf-anchor'), node_dest
1528
+ end
1505
1529
  end
1506
1530
  end
1507
1531
  # NOTE: import_page automatically advances to next page afterwards
1508
- # QUESTION: should we add destination to top of imported page?
1509
1532
  if (pgnums = node.attr 'pages')
1510
1533
  (resolve_pagenums pgnums).each_with_index do |pgnum, idx|
1511
1534
  if idx == 0
1512
- import_page image_path, page: pgnum, replace: page.empty?, &add_dest_block
1535
+ import_page image_path, page: pgnum, replace: replace, &add_dest_block
1513
1536
  else
1514
1537
  import_page image_path, page: pgnum, replace: true
1515
1538
  end
1516
1539
  end
1517
1540
  else
1518
- import_page image_path, page: [(node.attr 'page', nil, 1).to_i, 1].max, replace: page.empty?, &add_dest_block
1541
+ import_page image_path, page: [(node.attr 'page', nil, 1).to_i, 1].max, replace: replace, &add_dest_block
1519
1542
  end
1520
1543
  return
1521
1544
  else
@@ -1532,14 +1555,16 @@ module Asciidoctor
1532
1555
 
1533
1556
  alignment = (alignment = node.attr 'align') ?
1534
1557
  ((BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left) :
1535
- (resolve_alignment_from_role node.roles) || (@theme.image_align&.to_sym || :left)
1558
+ (resolve_alignment_from_role node.roles) || @theme.image_align&.to_sym || :left
1536
1559
  # TODO: support cover (aka canvas) image layout using "canvas" (or "cover") role
1537
1560
  width = resolve_explicit_width node.attributes, bounds_width: (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true
1538
1561
  # TODO: add `to_pt page_width` method to ViewportWidth type
1539
1562
  width = (width.to_f / 100) * page_width if ViewportWidth === width
1540
1563
 
1564
+ caption_end = @theme.image_caption_end&.to_sym || :bottom
1565
+ caption_max_width = @theme.image_caption_max_width
1541
1566
  # NOTE: if width is not set explicitly and max-width is fit-content, caption height may not be accurate
1542
- caption_h = node.title? ? (layout_caption node, category: :image, side: :bottom, block_align: alignment, block_width: width, max_width: @theme.image_caption_max_width, dry_run: true, force_top_margin: true) : 0
1567
+ caption_h = node.title? ? (ink_caption node, category: :image, end: caption_end, block_align: alignment, block_width: width, max_width: caption_max_width, dry_run: true, force_top_margin: caption_end == :bottom) : 0
1543
1568
 
1544
1569
  align_to_page = node.option? 'align-to-page'
1545
1570
  pinned = opts[:pinned]
@@ -1575,14 +1600,14 @@ module Asciidoctor
1575
1600
  end
1576
1601
  rendered_w = (svg_obj.resize height: (rendered_h = available_h)).output_width if rendered_h > available_h
1577
1602
  end
1578
- image_y = y
1579
- image_cursor = cursor
1580
1603
  add_dest_for_block node if node.id
1581
1604
  # NOTE: workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1582
1605
  # breakage occurs when running content (stamps) are added to page
1583
1606
  update_colors if graphic_state.color_space.empty?
1584
- # NOTE: cursor advances automatically
1585
- svg_obj.draw
1607
+ ink_caption node, category: :image, end: :top, block_align: alignment, block_width: rendered_w, max_width: caption_max_width if caption_end == :top && node.title?
1608
+ image_y = y
1609
+ image_cursor = cursor
1610
+ svg_obj.draw # NOTE: cursor advances automatically
1586
1611
  svg_obj.document.warnings.each do |img_warning|
1587
1612
  log :warn, %(problem encountered in image: #{image_path}; #{img_warning})
1588
1613
  end unless scratch?
@@ -1606,12 +1631,13 @@ module Asciidoctor
1606
1631
  end
1607
1632
  rendered_w, rendered_h = image_info.calc_image_dimensions height: available_h if rendered_h > available_h
1608
1633
  end
1609
- image_y = y
1610
- image_cursor = cursor
1611
1634
  add_dest_for_block node if node.id
1612
1635
  # NOTE: workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1613
1636
  # breakage occurs when running content (stamps) are added to page
1614
1637
  update_colors if graphic_state.color_space.empty?
1638
+ ink_caption node, category: :image, end: :top, block_align: alignment, block_width: rendered_w, max_width: caption_max_width if caption_end == :top && node.title?
1639
+ image_y = y
1640
+ image_cursor = cursor
1615
1641
  # NOTE: specify both width and height to avoid recalculation
1616
1642
  embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
1617
1643
  draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? && (node.has_role? 'noborder')
@@ -1622,7 +1648,7 @@ module Asciidoctor
1622
1648
  move_down rendered_h if y == image_y
1623
1649
  end
1624
1650
  end
1625
- layout_caption node, category: :image, side: :bottom, block_align: alignment, block_width: rendered_w, max_width: @theme.image_caption_max_width if node.title?
1651
+ ink_caption node, category: :image, end: :bottom, block_align: alignment, block_width: rendered_w, max_width: caption_max_width if caption_end == :bottom && node.title?
1626
1652
  theme_margin :block, :bottom, (next_enclosed_block node) unless pinned
1627
1653
  rescue => e
1628
1654
  raise if ::StopIteration === e
@@ -1631,7 +1657,7 @@ module Asciidoctor
1631
1657
  end
1632
1658
 
1633
1659
  def draw_image_border top, w, h, alignment
1634
- if @theme.image_border_width > 0 && @theme.image_border_color
1660
+ if (Array @theme.image_border_width).any? {|it| it&.> 0 } && (@theme.image_border_color || @theme.base_border_color)
1635
1661
  if (@theme.image_border_fit || 'content') == 'auto'
1636
1662
  bb_width = bounds.width
1637
1663
  elsif alignment == :center
@@ -1662,9 +1688,9 @@ module Asciidoctor
1662
1688
  alignment = (alignment = node.attr 'align') ?
1663
1689
  ((BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left) :
1664
1690
  (resolve_alignment_from_role node.roles) || (@theme.image_align&.to_sym || :left)
1665
- layout_prose alt_text_template % alt_text_vars, align: alignment, margin: 0, normalize: false, single_line: true
1691
+ ink_prose alt_text_template % alt_text_vars, align: alignment, margin: 0, normalize: false, single_line: true
1666
1692
  end
1667
- layout_caption node, category: :image, side: :bottom if node.title?
1693
+ ink_caption node, category: :image, end: :bottom if node.title?
1668
1694
  theme_margin :block, :bottom, (next_enclosed_block node) unless opts[:pinned]
1669
1695
  nil
1670
1696
  end
@@ -1673,8 +1699,8 @@ module Asciidoctor
1673
1699
  add_dest_for_block node if node.id
1674
1700
  audio_path = node.media_uri node.attr 'target'
1675
1701
  play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
1676
- layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
1677
- layout_caption node, labeled: false, side: :bottom if node.title?
1702
+ ink_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
1703
+ ink_caption node, labeled: false, end: :bottom if node.title?
1678
1704
  theme_margin :block, :bottom, (next_enclosed_block node)
1679
1705
  end
1680
1706
 
@@ -1701,8 +1727,8 @@ module Asciidoctor
1701
1727
  if poster.nil_or_empty?
1702
1728
  add_dest_for_block node if node.id
1703
1729
  play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
1704
- layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
1705
- layout_caption node, labeled: false, side: :bottom if node.title?
1730
+ ink_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
1731
+ ink_caption node, labeled: false, end: :bottom if node.title?
1706
1732
  theme_margin :block, :bottom, (next_enclosed_block node)
1707
1733
  else
1708
1734
  original_attributes = node.attributes.dup
@@ -1717,7 +1743,8 @@ module Asciidoctor
1717
1743
 
1718
1744
  # QUESTION: can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
1719
1745
  def convert_listing_or_literal node
1720
- wrap_ext = source_chunks = bg_color_override = font_color_override = adjusted_font_size = nil
1746
+ extensions = []
1747
+ source_chunks = bg_color_override = font_color_override = adjusted_font_size = nil
1721
1748
  theme_font :code do
1722
1749
  # HACK: disable built-in syntax highlighter; must be done before calling node.content!
1723
1750
  if node.style == 'source' && (highlighter = (syntax_hl = node.document.syntax_highlighter)&.highlight? && syntax_hl.name)
@@ -1797,7 +1824,7 @@ module Asciidoctor
1797
1824
  if (node.option? 'linenums') || (node.attr? 'linenums')
1798
1825
  linenums = (node.attr 'start', 1).to_i
1799
1826
  postprocess = true
1800
- wrap_ext = FormattedText::SourceWrap
1827
+ extensions << FormattedText::SourceWrap
1801
1828
  elsif conum_mapping || highlight_lines
1802
1829
  postprocess = true
1803
1830
  end
@@ -1814,7 +1841,7 @@ module Asciidoctor
1814
1841
  else
1815
1842
  if (node.option? 'linenums') || (node.attr? 'linenums')
1816
1843
  formatter_opts = { line_numbers: true, start_line: (node.attr 'start', 1).to_i }
1817
- wrap_ext = FormattedText::SourceWrap
1844
+ extensions << FormattedText::SourceWrap
1818
1845
  else
1819
1846
  formatter_opts = {}
1820
1847
  end
@@ -1848,14 +1875,13 @@ module Asciidoctor
1848
1875
  tare_first_page_content_stream do
1849
1876
  theme_fill_and_stroke_block :code, extent, background_color: bg_color_override, caption_node: node
1850
1877
  end
1851
- pad_box @theme.code_padding do
1878
+ pad_box @theme.code_padding, node do
1852
1879
  theme_font :code do
1853
- ::Prawn::Text::Formatted::Box.extensions << wrap_ext if wrap_ext
1854
1880
  typeset_formatted_text source_chunks, (calc_line_metrics @base_line_height),
1855
1881
  color: (font_color_override || @theme.code_font_color || @font_color),
1856
- size: adjusted_font_size
1857
- ensure
1858
- ::Prawn::Text::Formatted::Box.extensions.pop if wrap_ext
1882
+ size: adjusted_font_size,
1883
+ bottom_gutter: @bottom_gutters[-1][node],
1884
+ extensions: extensions.empty? ? nil : extensions
1859
1885
  end
1860
1886
  end
1861
1887
  end
@@ -2001,6 +2027,7 @@ module Asciidoctor
2001
2027
 
2002
2028
  base_header_cell_data = nil
2003
2029
  header_cell_line_metrics = nil
2030
+ body_cell_padding = expand_padding_value theme.table_cell_padding
2004
2031
 
2005
2032
  table_data = []
2006
2033
  theme_font :table do
@@ -2015,7 +2042,7 @@ module Asciidoctor
2015
2042
  table_header_size = head_rows.size
2016
2043
  head_font_info = font_info
2017
2044
  head_line_metrics = calc_line_metrics theme.table_head_line_height || theme.table_cell_line_height || @base_line_height
2018
- head_cell_padding = expand_padding_value theme.table_head_cell_padding || theme.table_cell_padding
2045
+ head_cell_padding = ((head_cell_padding = theme.table_head_cell_padding) ? (expand_padding_value head_cell_padding) : body_cell_padding).dup
2019
2046
  head_cell_padding[0] += head_line_metrics.padding_top
2020
2047
  head_cell_padding[2] += head_line_metrics.padding_bottom
2021
2048
  # QUESTION: why doesn't text transform inherit from table?
@@ -2054,7 +2081,6 @@ module Asciidoctor
2054
2081
  text_color: @font_color,
2055
2082
  }
2056
2083
  body_cell_line_metrics = calc_line_metrics (theme.table_cell_line_height || @base_line_height)
2057
- body_cell_padding = expand_padding_value theme.table_cell_padding
2058
2084
  (body_rows + node.rows[:foot]).each do |row|
2059
2085
  table_data << (row.map do |cell|
2060
2086
  cell_data = base_cell_data.merge \
@@ -2121,7 +2147,7 @@ module Asciidoctor
2121
2147
  end
2122
2148
  # NOTE: line metrics get applied when AsciiDoc content is converted
2123
2149
  cell_line_metrics = nil
2124
- asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self, (cell_data.merge content: cell.inner_document, padding: body_cell_padding.dup)
2150
+ asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self, (cell_data.merge content: cell.inner_document, padding: body_cell_padding)
2125
2151
  cell_data = { content: asciidoc_cell }
2126
2152
  end
2127
2153
  if cell_line_metrics
@@ -2159,45 +2185,36 @@ module Asciidoctor
2159
2185
  end
2160
2186
  end
2161
2187
 
2162
- # NOTE: Prawn aborts if table data is empty, so ensure there's at least one row
2188
+ # NOTE: Prawn crashes if table data is empty, so ensure there's at least one row
2163
2189
  if table_data.empty?
2164
2190
  log(:warn) { message_with_context 'no rows found in table', source_location: node.source_location }
2165
2191
  table_data << ::Array.new([node.columns.size, 1].max) { { content: '' } }
2166
2192
  end
2167
2193
 
2168
- border_width = {}
2169
- table_border_color = theme.table_border_color || ((Array theme.table_grid_color).size == 2 ? nil : theme.table_grid_color) || theme.base_border_color
2170
- table_border_style = theme.table_border_style&.to_sym || :solid
2171
- table_border_width = theme.table_border_width
2172
- if table_header_size
2173
- head_border_bottom_color = theme.table_head_border_bottom_color || table_border_color
2174
- head_border_bottom_style = (theme.table_head_border_bottom_style || table_border_style).to_sym
2175
- head_border_bottom_width = theme.table_head_border_bottom_width || table_border_width
2176
- end
2177
- [:top, :bottom, :left, :right].each {|edge| border_width[edge] = table_border_width }
2194
+ rect_side_names = [:top, :right, :bottom, :left]
2195
+ grid_axis_names = [:rows, :cols]
2196
+ border_color = (rect_side_names.zip expand_rect_values theme.table_border_color, 'transparent').to_h
2197
+ border_style = (rect_side_names.zip (expand_rect_values theme.table_border_style, :solid).map(&:to_sym)).to_h
2198
+ border_width = (rect_side_names.zip expand_rect_values theme.table_border_width, 0).to_h
2199
+ grid_color = (grid_axis_names.zip expand_grid_values (theme.table_grid_color || [border_color[:top], border_color[:left]]), 'transparent').to_h
2200
+ grid_style = (grid_axis_names.zip (expand_grid_values (theme.table_grid_style || [border_style[:top], border_style[:left]]), :solid).map(&:to_sym)).to_h
2201
+ grid_width = (grid_axis_names.zip expand_grid_values (theme.table_grid_width || [border_width[:top], border_width[:left]]), 0).to_h
2178
2202
 
2179
- table_grid_color = theme.table_grid_color || table_border_color
2180
- if ::Array === (table_grid_style = theme.table_grid_style || table_border_style)
2181
- table_grid_style = table_grid_style.map(&:to_sym)
2182
- else
2183
- table_grid_style = [table_grid_style.to_sym]
2184
- end
2185
- if ::Array === (table_grid_width = theme.table_grid_width || theme.table_border_width)
2186
- border_width[:rows] = table_grid_width[0]
2187
- border_width[:cols] = table_grid_width[1]
2188
- else
2189
- [:cols, :rows].each {|edge| border_width[edge] = table_grid_width }
2203
+ if table_header_size
2204
+ head_border_bottom_color = theme.table_head_border_bottom_color || grid_color[:rows]
2205
+ head_border_bottom_style = theme.table_head_border_bottom_style&.to_sym || grid_style[:rows]
2206
+ head_border_bottom_width = theme.table_head_border_bottom_width || (grid_width[:rows] * 2.5)
2190
2207
  end
2191
2208
 
2192
2209
  case (grid = node.attr 'grid', 'all', 'table-grid')
2193
2210
  when 'all'
2194
2211
  # keep inner borders
2195
2212
  when 'cols'
2196
- border_width[:rows] = 0
2213
+ grid_width[:rows] = 0
2197
2214
  when 'rows'
2198
- border_width[:cols] = 0
2215
+ grid_width[:cols] = 0
2199
2216
  else # none
2200
- border_width[:rows] = border_width[:cols] = 0
2217
+ grid_width[:rows] = grid_width[:cols] = 0
2201
2218
  end
2202
2219
 
2203
2220
  case (frame = node.attr 'frame', 'all', 'table-frame')
@@ -2227,20 +2244,15 @@ module Asciidoctor
2227
2244
  alignment = theme.table_align&.to_sym || :left
2228
2245
  end
2229
2246
 
2230
- caption_side = theme.table_caption_side&.to_sym || :top
2247
+ caption_end = theme.table_caption_end&.to_sym || :top
2231
2248
  caption_max_width = theme.table_caption_max_width || 'fit-content'
2232
2249
 
2233
2250
  table_settings = {
2234
2251
  header: table_header_size,
2235
2252
  # NOTE: position is handled by this method
2236
2253
  position: :left,
2237
- cell_style: {
2238
- # NOTE: the border color and style of the outer frame is set later
2239
- border_color: table_grid_color,
2240
- border_lines: table_grid_style,
2241
- # NOTE: the border width is set later
2242
- border_width: 0,
2243
- },
2254
+ # NOTE: the border color, style, and width of the outer frame is set in the table callback block
2255
+ cell_style: { border_color: grid_color.values, border_lines: grid_style.values, border_width: grid_width.values },
2244
2256
  width: table_width,
2245
2257
  column_widths: column_widths,
2246
2258
  }
@@ -2261,7 +2273,7 @@ module Asciidoctor
2261
2273
  table table_data, table_settings do
2262
2274
  # NOTE: call width to capture resolved table width
2263
2275
  table_width = width
2264
- @pdf.layout_table_caption node, alignment, table_width, caption_max_width if node.title? && caption_side == :top
2276
+ @pdf.ink_table_caption node, alignment, table_width, caption_max_width if node.title? && caption_end == :top
2265
2277
  # NOTE: align using padding instead of bounding_box as prawn-table does
2266
2278
  # using a bounding_box across pages mangles the margin box of subsequent pages
2267
2279
  if alignment != :left && table_width != (this_bounds = @pdf.bounds).width
@@ -2282,7 +2294,7 @@ module Asciidoctor
2282
2294
  end if table_header_size
2283
2295
  else
2284
2296
  # apply the grid setting first across all cells
2285
- cells.border_width = [border_width[:rows], border_width[:cols], border_width[:rows], border_width[:cols]]
2297
+ cells.border_width = [grid_width[:rows], grid_width[:cols], grid_width[:rows], grid_width[:cols]]
2286
2298
 
2287
2299
  if table_header_size
2288
2300
  (rows table_header_size - 1).tap do |r|
@@ -2299,19 +2311,19 @@ module Asciidoctor
2299
2311
 
2300
2312
  # top edge of table
2301
2313
  (rows 0).tap do |r|
2302
- r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, table_border_style, border_width[:top]
2314
+ r.border_top_color, r.border_top_line, r.border_top_width = border_color[:top], border_style[:top], border_width[:top]
2303
2315
  end
2304
2316
  # right edge of table
2305
2317
  (columns num_cols - 1).tap do |r|
2306
- r.border_right_color, r.border_right_line, r.border_right_width = table_border_color, table_border_style, border_width[:right]
2318
+ r.border_right_color, r.border_right_line, r.border_right_width = border_color[:right], border_style[:right], border_width[:right]
2307
2319
  end
2308
2320
  # bottom edge of table
2309
2321
  (rows num_rows - 1).tap do |r|
2310
- r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, table_border_style, border_width[:bottom]
2322
+ r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = border_color[:bottom], border_style[:bottom], border_width[:bottom]
2311
2323
  end
2312
2324
  # left edge of table
2313
2325
  (columns 0).tap do |r|
2314
- r.border_left_color, r.border_left_line, r.border_left_width = table_border_color, table_border_style, border_width[:left]
2326
+ r.border_left_color, r.border_left_line, r.border_left_width = border_color[:left], border_style[:left], border_width[:left]
2315
2327
  end
2316
2328
  end
2317
2329
 
@@ -2334,7 +2346,7 @@ module Asciidoctor
2334
2346
  bounds.subtract_left_padding left_padding
2335
2347
  bounds.subtract_right_padding right_padding if right_padding
2336
2348
  end
2337
- layout_table_caption node, alignment, table_width, caption_max_width, caption_side if node.title? && caption_side == :bottom
2349
+ ink_table_caption node, alignment, table_width, caption_max_width, caption_end if node.title? && caption_end == :bottom
2338
2350
  theme_margin :block, :bottom, (next_enclosed_block node)
2339
2351
  rescue ::Prawn::Errors::CannotFit
2340
2352
  log :error, (message_with_context 'cannot fit contents of table cell into specified column width', source_location: node.source_location)
@@ -2342,12 +2354,14 @@ module Asciidoctor
2342
2354
 
2343
2355
  def convert_thematic_break node
2344
2356
  theme_margin :thematic_break, :top
2345
- stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width, line_style: (@theme.thematic_break_border_style&.to_sym || :solid)
2357
+ stroke_horizontal_rule @theme.thematic_break_border_color,
2358
+ line_width: @theme.thematic_break_border_width,
2359
+ line_style: (@theme.thematic_break_border_style&.to_sym || :solid)
2346
2360
  theme_margin :thematic_break, ((block_next = next_enclosed_block node) ? :bottom : :top), block_next || true
2347
2361
  end
2348
2362
 
2349
2363
  def convert_toc node, opts = {}
2350
- # NOTE: only allow document to have a single toc
2364
+ # NOTE: only allow document to have a single managed toc
2351
2365
  return if @toc_extent
2352
2366
  is_macro = (placement = opts[:placement] || 'macro') == 'macro'
2353
2367
  if ((doc = node.document).attr? 'toc-placement', placement) && (doc.attr? 'toc') && doc.sections?
@@ -2356,11 +2370,11 @@ module Asciidoctor
2356
2370
  start_new_page if @ppbook && verso_page? && !(is_macro && (node.option? 'nonfacing'))
2357
2371
  end
2358
2372
  add_dest_for_block node, id: (node.id || 'toc') if is_macro
2359
- allocate_toc doc, (doc.attr 'toclevels', 2).to_i, cursor, (use_title_page = is_book || (doc.attr? 'title-page'))
2360
- @index.start_page_number = @toc_extent.to.page + 1 if use_title_page && @theme.page_numbering_start_at == 'after-toc'
2373
+ toc_extent = @toc_extent = allocate_toc doc, (doc.attr 'toclevels', 2).to_i, cursor, (title_page_on = is_book || (doc.attr? 'title-page'))
2374
+ @index.start_page_number = toc_extent.to.page + 1 if title_page_on && @theme.page_numbering_start_at == 'after-toc'
2361
2375
  if is_macro
2362
- @disable_running_content[:header] += @toc_extent.page_range if node.option? 'noheader'
2363
- @disable_running_content[:footer] += @toc_extent.page_range if node.option? 'nofooter'
2376
+ @disable_running_content[:header] += toc_extent.page_range if node.option? 'noheader'
2377
+ @disable_running_content[:footer] += toc_extent.page_range if node.option? 'nofooter'
2364
2378
  end
2365
2379
  end
2366
2380
  nil
@@ -2378,7 +2392,7 @@ module Asciidoctor
2378
2392
 
2379
2393
  if at_page_top?
2380
2394
  if page_layout && page_layout != page.layout && page.empty?
2381
- delete_page
2395
+ delete_current_page
2382
2396
  advance_page layout: page_layout
2383
2397
  end
2384
2398
  elsif page_layout
@@ -2388,8 +2402,9 @@ module Asciidoctor
2388
2402
  end
2389
2403
  end
2390
2404
 
2391
- def convert_index_section _node
2405
+ def convert_index_section node
2392
2406
  space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
2407
+ pagenum_sequence_style = node.document.attr 'index-pagenum-sequence-style'
2393
2408
  column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true do
2394
2409
  def @bounding_box.move_past_bottom *args # rubocop:disable Lint/NestedMethodDefinition
2395
2410
  super(*args)
@@ -2398,12 +2413,12 @@ module Asciidoctor
2398
2413
  @index.categories.each do |category|
2399
2414
  # NOTE: cursor method always returns 0 inside column_box; breaks reference_bounds.move_past_bottom
2400
2415
  bounds.move_past_bottom if space_needed_for_category > y - reference_bounds.absolute_bottom
2401
- layout_prose category.name,
2416
+ ink_prose category.name,
2402
2417
  align: :left,
2403
2418
  inline_format: false,
2404
2419
  margin_bottom: @theme.description_list_term_spacing,
2405
- style: @theme.description_list_term_font_style.to_sym
2406
- category.terms.each {|term| convert_index_list_item term }
2420
+ style: @theme.description_list_term_font_style&.to_sym
2421
+ category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style }
2407
2422
  # NOTE: see previous note for why we can't use margin_bottom method
2408
2423
  if @theme.prose_margin_bottom > y - reference_bounds.absolute_bottom
2409
2424
  bounds.move_past_bottom
@@ -2415,21 +2430,32 @@ module Asciidoctor
2415
2430
  nil
2416
2431
  end
2417
2432
 
2418
- def convert_index_list_item term
2433
+ def convert_index_list_item term, pagenum_sequence_style = nil
2419
2434
  text = escape_xml term.name
2420
2435
  unless term.container?
2421
2436
  if @media == 'screen'
2422
- pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2437
+ case pagenum_sequence_style
2438
+ when 'page'
2439
+ pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2440
+ when 'range'
2441
+ first_anchor_per_page = term.dests.each_with_object({}) {|dest, accum| accum[dest[:page]] ||= dest[:anchor] }
2442
+ pagenums = (consolidate_ranges first_anchor_per_page.keys).map do |range|
2443
+ anchor = first_anchor_per_page[(range.include? '-') ? (range.partition '-')[0] : range]
2444
+ %(<a anchor="#{anchor}">#{range}</a>)
2445
+ end
2446
+ else # term
2447
+ pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2448
+ end
2423
2449
  else
2424
- pagenums = consolidate_ranges term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page] } # rubocop:disable Lint/AmbiguousBlockAssociation
2450
+ pagenums = consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq
2425
2451
  end
2426
2452
  text = %(#{text}, #{pagenums.join ', '})
2427
2453
  end
2428
2454
  subterm_indent = @theme.description_list_description_indent
2429
- layout_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
2455
+ ink_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
2430
2456
  indent subterm_indent do
2431
2457
  term.subterms.each do |subterm|
2432
- convert_index_list_item subterm
2458
+ convert_index_list_item subterm, pagenum_sequence_style
2433
2459
  end
2434
2460
  end unless term.leaf?
2435
2461
  end
@@ -2527,14 +2553,14 @@ module Asciidoctor
2527
2553
  end
2528
2554
 
2529
2555
  def convert_inline_icon node
2530
- if node.document.attr? 'icons', 'font'
2556
+ if (icons = (doc = node.document).attr 'icons') == 'font'
2531
2557
  if (icon_name = node.target).include? '@'
2532
2558
  icon_name, icon_set = icon_name.split '@', 2
2533
2559
  explicit_icon_set = true
2534
2560
  elsif (icon_set = node.attr 'set')
2535
2561
  explicit_icon_set = true
2536
2562
  else
2537
- icon_set = node.document.attr 'icon-set', 'fa'
2563
+ icon_set = doc.attr 'icon-set', 'fa'
2538
2564
  end
2539
2565
  if icon_set == 'fa' || !(IconSets.include? icon_set)
2540
2566
  icon_set = 'fa'
@@ -2580,6 +2606,14 @@ module Asciidoctor
2580
2606
  log :warn, %(#{icon_name} is not a valid icon name in the #{icon_set} icon set)
2581
2607
  %([#{node.attr 'alt'}&#93;)
2582
2608
  end
2609
+ elsif icons
2610
+ image_path = ::File.absolute_path %(#{icon_name = node.target}.#{image_format = doc.attr 'icontype', 'png'}), (doc.attr 'iconsdir')
2611
+ if ::File.readable? image_path
2612
+ %(<img src="#{image_path}" format="#{image_format}" alt="#{node.attr 'alt'}" fit="line">)
2613
+ else
2614
+ log :warn, %(image icon for '#{icon_name}' not found or not readable: #{image_path})
2615
+ %([#{icon_name}&#93;)
2616
+ end
2583
2617
  else
2584
2618
  %([#{node.attr 'alt'}&#93;)
2585
2619
  end
@@ -2616,15 +2650,16 @@ module Asciidoctor
2616
2650
  end
2617
2651
 
2618
2652
  def convert_inline_indexterm node
2653
+ visible = node.type == :visible
2619
2654
  if scratch?
2620
- node.type == :visible ? node.text : ''
2655
+ visible ? node.text : ''
2621
2656
  else
2622
2657
  # NOTE: initialize index in case converter is called before PDF is initialized
2623
2658
  @index ||= IndexCatalog.new
2624
2659
  # NOTE: page number (:page key) is added by InlineDestinationMarker
2625
2660
  dest = { anchor: (anchor_name = @index.next_anchor_name) }
2626
- anchor = %(<a id="#{anchor_name}" type="indexterm">#{DummyText}</a>)
2627
- if node.type == :visible
2661
+ anchor = %(<a id="#{anchor_name}" type="indexterm"#{visible ? ' visible="true"' : ''}>#{DummyText}</a>)
2662
+ if visible
2628
2663
  visible_term = node.text
2629
2664
  @index.store_primary_term (sanitize visible_term), dest
2630
2665
  %(#{anchor}#{visible_term})
@@ -2671,8 +2706,10 @@ module Asciidoctor
2671
2706
  open, close, is_tag = ['<sub>', '</sub>', true]
2672
2707
  when :double
2673
2708
  open, close, is_tag = [theme.quotes[0], theme.quotes[1], false]
2709
+ quotes = true
2674
2710
  when :single
2675
2711
  open, close, is_tag = [theme.quotes[2], theme.quotes[3], false]
2712
+ quotes = true
2676
2713
  when :mark
2677
2714
  open, close, is_tag = ['<mark>', '</mark>', true]
2678
2715
  else
@@ -2681,6 +2718,11 @@ module Asciidoctor
2681
2718
 
2682
2719
  inner_text = node.text
2683
2720
 
2721
+ if quotes && (len = inner_text.length) > 3 &&
2722
+ (inner_text.end_with? '...') && !((inner_text_trunc = inner_text.slice 0, len - 3).end_with? ?\\)
2723
+ inner_text = inner_text_trunc + '&#8230;'
2724
+ end
2725
+
2684
2726
  if (roles = node.role)
2685
2727
  roles.split.each do |role|
2686
2728
  if (text_transform = theme[%(role_#{role}_text_transform)])
@@ -2697,7 +2739,8 @@ module Asciidoctor
2697
2739
  node.id ? %(<a id="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
2698
2740
  end
2699
2741
 
2700
- def layout_title_page doc
2742
+ # Returns a Boolean indicating whether the title page was created
2743
+ def ink_title_page doc
2701
2744
  return unless doc.header? && !doc.notitle && @theme.title_page != false
2702
2745
 
2703
2746
  # NOTE: a new page may have already been started at this point, so decide what to do with it
@@ -2724,7 +2767,7 @@ module Asciidoctor
2724
2767
  font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style
2725
2768
 
2726
2769
  # QUESTION: allow alignment per element on title page?
2727
- title_align = (@theme.title_page_align || @base_align).to_sym
2770
+ title_align = (@theme.title_page_text_align || @base_text_align).to_sym
2728
2771
 
2729
2772
  if @theme.title_page_logo_display != 'none' && (logo_image_path = (doc.attr 'title-logo-image') || (logo_image_from_theme = @theme.title_page_logo_image))
2730
2773
  if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
@@ -2773,7 +2816,7 @@ module Asciidoctor
2773
2816
  move_down @theme.title_page_title_margin_top || 0
2774
2817
  indent (@theme.title_page_title_margin_left || 0), (@theme.title_page_title_margin_right || 0) do
2775
2818
  theme_font :title_page_title do
2776
- layout_prose doctitle.main, align: title_align, margin: 0
2819
+ ink_prose doctitle.main, align: title_align, margin: 0
2777
2820
  end
2778
2821
  end
2779
2822
  move_down @theme.title_page_title_margin_bottom || 0
@@ -2782,7 +2825,7 @@ module Asciidoctor
2782
2825
  move_down @theme.title_page_subtitle_margin_top || 0
2783
2826
  indent (@theme.title_page_subtitle_margin_left || 0), (@theme.title_page_subtitle_margin_right || 0) do
2784
2827
  theme_font :title_page_subtitle do
2785
- layout_prose subtitle, align: title_align, margin: 0
2828
+ ink_prose subtitle, align: title_align, margin: 0
2786
2829
  end
2787
2830
  end
2788
2831
  move_down @theme.title_page_subtitle_margin_bottom || 0
@@ -2807,7 +2850,7 @@ module Asciidoctor
2807
2850
  end
2808
2851
  end.join @theme.title_page_authors_delimiter
2809
2852
  theme_font :title_page_authors do
2810
- layout_prose authors, align: title_align, margin: 0, normalize: true
2853
+ ink_prose authors, align: title_align, margin: 0, normalize: true
2811
2854
  end
2812
2855
  end
2813
2856
  move_down @theme.title_page_authors_margin_bottom || 0
@@ -2820,17 +2863,18 @@ module Asciidoctor
2820
2863
  end
2821
2864
  indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
2822
2865
  theme_font :title_page_revision do
2823
- layout_prose revision_text, align: title_align, margin: 0, normalize: false
2866
+ ink_prose revision_text, align: title_align, margin: 0, normalize: false
2824
2867
  end
2825
2868
  end
2826
2869
  move_down @theme.title_page_revision_margin_bottom || 0
2827
2870
  end
2828
2871
  end
2829
2872
 
2830
- layout_prose DummyText, margin: 0, line_height: 1, normalize: false if page.empty?
2873
+ ink_prose DummyText, margin: 0, line_height: 1, normalize: false if page.empty?
2874
+ true
2831
2875
  end
2832
2876
 
2833
- def layout_cover_page doc, face
2877
+ def ink_cover_page doc, face
2834
2878
  image_path, image_opts = resolve_background_image doc, @theme, %(#{face}-cover-image), theme_key: %(cover_#{face}_image).to_sym, symbolic_paths: ['', '~']
2835
2879
  if image_path
2836
2880
  if image_path.empty?
@@ -2876,71 +2920,109 @@ module Asciidoctor
2876
2920
 
2877
2921
  def start_new_chapter chapter
2878
2922
  start_new_page unless at_page_top?
2879
- # TODO: must call update_colors before advancing to next page if start_new_page is called in layout_chapter_title
2923
+ # TODO: must call update_colors before advancing to next page if start_new_page is called in ink_chapter_title
2880
2924
  start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
2881
2925
  end
2882
2926
 
2883
2927
  alias start_new_part start_new_chapter
2884
2928
 
2885
2929
  def arrange_section sect, title, opts
2886
- theme_font :heading, level: (hlevel = opts[:level]) do
2887
- # FIXME: this height doesn't account for impact of text transform or inline formatting
2888
- heading_height =
2889
- (height_of_typeset_text title) +
2890
- (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
2891
- (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom)
2892
- heading_height += @theme.heading_min_height_after if sect.blocks? && @theme.heading_min_height_after
2893
- start_new_page unless cursor > heading_height
2894
- nil
2930
+ if sect.option? 'breakable'
2931
+ orphaned = nil
2932
+ dry_run single_page: true do
2933
+ start_page = page
2934
+ theme_font :heading, level: opts[:level] do
2935
+ if opts[:part]
2936
+ ink_part_title sect, title, opts
2937
+ elsif opts[:chapterlike]
2938
+ ink_chapter_title sect, title, opts
2939
+ else
2940
+ ink_general_heading sect, title, opts
2941
+ end
2942
+ end
2943
+ if page == start_page
2944
+ page.tare_content_stream
2945
+ orphaned = stop_if_first_page_empty { traverse sect }
2946
+ end
2947
+ end
2948
+ start_new_page if orphaned
2949
+ else
2950
+ theme_font :heading, level: (hlevel = opts[:level]) do
2951
+ h_padding_t, h_padding_r, h_padding_b, h_padding_l = expand_padding_value @theme[%(heading_h#{hlevel}_padding)]
2952
+ h_fits = indent h_padding_l, h_padding_r do
2953
+ # FIXME: this height doesn't account for impact of text transform or inline formatting
2954
+ heading_h = (height_of_typeset_text title) +
2955
+ (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
2956
+ (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom) + h_padding_t + h_padding_b
2957
+ heading_h += @theme.heading_min_height_after if @theme.heading_min_height_after && sect.blocks?
2958
+ cursor >= heading_h
2959
+ end
2960
+ start_new_page unless h_fits
2961
+ end
2895
2962
  end
2963
+ nil
2896
2964
  end
2897
2965
 
2898
- def layout_chapter_title node, title, opts = {}
2899
- layout_general_heading node, title, (opts.merge outdent: true)
2966
+ def ink_chapter_title node, title, opts = {}
2967
+ ink_general_heading node, title, (opts.merge outdent: true)
2900
2968
  end
2901
2969
 
2902
- alias layout_part_title layout_chapter_title
2970
+ alias ink_part_title ink_chapter_title
2903
2971
 
2904
- def layout_general_heading _node, title, opts = {}
2905
- layout_heading title, opts
2972
+ def ink_general_heading _node, title, opts = {}
2973
+ ink_heading title, opts
2906
2974
  end
2907
2975
 
2908
- # NOTE: layout_heading doesn't set the theme font because it's used for various types of headings
2909
- def layout_heading string, opts = {}
2910
- hlevel = opts[:level]
2976
+ # NOTE: ink_heading doesn't set the theme font because it's used for various types of headings
2977
+ def ink_heading string, opts = {}
2978
+ if (h_level = opts[:level])
2979
+ h_category = %(heading_h#{h_level})
2980
+ end
2911
2981
  unless (top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top))
2912
2982
  if at_page_top?
2913
- if hlevel && (top_margin = @theme[%(heading_h#{hlevel}_margin_page_top)] || @theme.heading_margin_page_top) > 0
2983
+ if h_category && (top_margin = @theme[%(#{h_category}_margin_page_top)] || @theme.heading_margin_page_top) > 0
2914
2984
  move_down top_margin
2915
2985
  end
2916
2986
  top_margin = 0
2917
2987
  else
2918
- top_margin = (hlevel ? @theme[%(heading_h#{hlevel}_margin_top)] : nil) || @theme.heading_margin_top
2988
+ top_margin = (h_category ? @theme[%(#{h_category}_margin_top)] : nil) || @theme.heading_margin_top
2919
2989
  end
2920
2990
  end
2921
- bot_margin = margin || (opts.delete :margin_bottom) || (hlevel ? @theme[%(heading_h#{hlevel}_margin_bottom)] : nil) || @theme.heading_margin_bottom
2991
+ bot_margin = margin || (opts.delete :margin_bottom) || (h_category ? @theme[%(#{h_category}_margin_bottom)] : nil) || @theme.heading_margin_bottom
2922
2992
  if (transform = resolve_text_transform opts)
2923
2993
  string = transform_text string, transform
2924
2994
  end
2925
2995
  outdent_section opts.delete :outdent do
2926
2996
  margin_top top_margin
2927
- # QUESTION: should we move inherited styles to typeset_text?
2928
- if (inherited = apply_text_decoration font_styles, :heading, hlevel).empty?
2929
- inline_format_opts = true
2930
- else
2931
- inline_format_opts = [{ inherited: inherited }]
2997
+ start_cursor = cursor
2998
+ start_page_number = page_number
2999
+ pad_box h_category ? @theme[%(#{h_category}_padding)] : nil do
3000
+ # QUESTION: should we move inherited styles to typeset_text?
3001
+ if (inherited = apply_text_decoration font_styles, :heading, h_level).empty?
3002
+ inline_format_opts = true
3003
+ else
3004
+ inline_format_opts = [{ inherited: inherited }]
3005
+ end
3006
+ typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
3007
+ color: @font_color,
3008
+ inline_format: inline_format_opts,
3009
+ align: @base_text_align.to_sym,
3010
+ }.merge(opts)
3011
+ end
3012
+ if h_category && @theme[%(#{h_category}_border_width)] &&
3013
+ (@theme[%(#{h_category}_border_color)] || @theme.base_border_color) && page_number == start_page_number
3014
+ float do
3015
+ bounding_box [bounds.left, start_cursor], width: bounds.width, height: start_cursor - cursor do
3016
+ theme_fill_and_stroke_bounds h_category
3017
+ end
3018
+ end
2932
3019
  end
2933
- typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
2934
- color: @font_color,
2935
- inline_format: inline_format_opts,
2936
- align: @base_align.to_sym,
2937
- }.merge(opts)
2938
3020
  margin_bottom bot_margin
2939
3021
  end
2940
3022
  end
2941
3023
 
2942
3024
  # NOTE: inline_format is true by default
2943
- def layout_prose string, opts = {}
3025
+ def ink_prose string, opts = {}
2944
3026
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || 0
2945
3027
  bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
2946
3028
  if (transform = resolve_text_transform opts)
@@ -2964,7 +3046,7 @@ module Asciidoctor
2964
3046
  typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
2965
3047
  color: @font_color,
2966
3048
  inline_format: [inline_format_opts],
2967
- align: @base_align.to_sym,
3049
+ align: @base_text_align.to_sym,
2968
3050
  }.merge(opts)
2969
3051
  margin_bottom bot_margin
2970
3052
  end
@@ -2987,13 +3069,13 @@ module Asciidoctor
2987
3069
  # The subject argument can either be a String or an AbstractNode. If
2988
3070
  # subject is an AbstractNode, only call this method if the node has a
2989
3071
  # title (i.e., subject.title? returns true).
2990
- def layout_caption subject, opts = {}
3072
+ def ink_caption subject, opts = {}
2991
3073
  if opts.delete :dry_run
2992
3074
  force_top_margin = !at_page_top? if (force_top_margin = opts.delete :force_top_margin).nil?
2993
3075
  return (dry_run keep_together: true, single_page: :enforce do
2994
3076
  # TODO: encapsulate this logic to force top margin to be applied
2995
3077
  margin_box.instance_variable_set :@y, margin_box.absolute_top + 0.0001 if force_top_margin
2996
- layout_caption subject, opts
3078
+ ink_caption subject, opts
2997
3079
  end).single_page_height
2998
3080
  end
2999
3081
  if ::Asciidoctor::AbstractBlock === subject
@@ -3001,21 +3083,23 @@ module Asciidoctor
3001
3083
  else
3002
3084
  string = subject.to_s
3003
3085
  end
3086
+ block_align = opts.delete :block_align
3087
+ block_width = opts.delete :block_width
3004
3088
  category_caption = (category = opts[:category]) ? %(#{category}_caption) : 'caption'
3089
+ caption_margin_outside = @theme[%(#{category_caption}_margin_outside)] || @theme.caption_margin_outside
3090
+ caption_margin_inside = @theme[%(#{category_caption}_margin_inside)] || @theme.caption_margin_inside
3005
3091
  container_width = bounds.width
3006
- block_align = opts.delete :block_align
3092
+ indent_by = [0, 0]
3007
3093
  if (align = @theme[%(#{category_caption}_align)] || @theme.caption_align)
3008
- align = align == 'inherit' ? (block_align || @base_align.to_sym) : align.to_sym
3094
+ align = align == 'inherit' ? (block_align || @base_text_align.to_sym) : align.to_sym
3009
3095
  else
3010
- align = @base_align.to_sym
3096
+ align = @base_text_align.to_sym
3011
3097
  end
3012
3098
  if (text_align = @theme[%(#{category_caption}_text_align)] || @theme.caption_text_align)
3013
3099
  text_align = text_align == 'inherit' ? align : text_align.to_sym
3014
3100
  else
3015
3101
  text_align = align
3016
3102
  end
3017
- indent_by = [0, 0]
3018
- block_width = opts.delete :block_width
3019
3103
  if (max_width = opts.delete :max_width) && max_width != 'none'
3020
3104
  if ::String === max_width
3021
3105
  if max_width.start_with? 'fit-content'
@@ -3047,9 +3131,7 @@ module Asciidoctor
3047
3131
  end
3048
3132
  end
3049
3133
  theme_font_cascade [:caption, category_caption] do
3050
- caption_margin_outside = @theme[%(#{category_caption}_margin_outside)] || @theme.caption_margin_outside
3051
- caption_margin_inside = @theme[%(#{category_caption}_margin_inside)] || @theme.caption_margin_inside
3052
- if ((opts.delete :side) || :top) == :top
3134
+ if ((opts.delete :end) || (opts.delete :side) || :top) == :top
3053
3135
  margin = { top: caption_margin_outside, bottom: caption_margin_inside }
3054
3136
  else
3055
3137
  margin = { top: caption_margin_inside, bottom: caption_margin_outside }
@@ -3057,8 +3139,13 @@ module Asciidoctor
3057
3139
  unless (inherited = apply_text_decoration [], :caption).empty?
3058
3140
  opts = opts.merge inherited
3059
3141
  end
3142
+ unless scratch? || !(bg_color = @theme[%(#{category_caption}_background_color)] || @theme.caption_background_color)
3143
+ caption_height = height_of_typeset_text string
3144
+ fill_at = [0, cursor + (margin[:top] || 0)]
3145
+ float { bounding_box(fill_at, width: container_width, height: caption_height) { fill_bounds bg_color } }
3146
+ end
3060
3147
  indent(*indent_by) do
3061
- layout_prose string, ({
3148
+ ink_prose string, ({
3062
3149
  margin_top: margin[:top],
3063
3150
  margin_bottom: margin[:bottom],
3064
3151
  align: text_align,
@@ -3072,35 +3159,45 @@ module Asciidoctor
3072
3159
  end
3073
3160
 
3074
3161
  # Render the caption for a table and return the height of the rendered content
3075
- def layout_table_caption node, table_alignment = :left, table_width = nil, max_width = nil, side = :top
3076
- layout_caption node, category: :table, side: side, block_align: table_alignment, block_width: table_width, max_width: max_width
3162
+ def ink_table_caption node, table_alignment = :left, table_width = nil, max_width = nil, end_ = :top
3163
+ ink_caption node, category: :table, end: end_, block_align: table_alignment, block_width: table_width, max_width: max_width
3077
3164
  end
3078
3165
 
3079
- def allocate_toc doc, toc_num_levels, toc_start_cursor, use_title_page
3080
- toc_start_page = page_number
3166
+ def allocate_toc doc, toc_num_levels, toc_start_cursor, title_page_on
3167
+ toc_start_page_number = page_number
3168
+ to_page = nil
3081
3169
  extent = dry_run onto: self do
3082
- layout_toc doc, toc_num_levels, toc_start_page, toc_start_cursor
3083
- margin_bottom @theme.block_margin_bottom unless use_title_page
3170
+ to_page = (ink_toc doc, toc_num_levels, toc_start_page_number, toc_start_cursor).end
3171
+ margin_bottom @theme.block_margin_bottom unless title_page_on
3172
+ end
3173
+ # NOTE: patch for custom converters that allocate extra TOC pages without actually creating them
3174
+ if to_page > extent.to.page
3175
+ extent.to.page = to_page
3176
+ extent.to.cursor = bounds.height
3084
3177
  end
3085
3178
  # NOTE: reserve pages for the toc; leaves cursor on page after last page in toc
3086
- if use_title_page
3179
+ if title_page_on
3087
3180
  extent.each_page { start_new_page }
3088
3181
  else
3089
3182
  extent.each_page {|first_page| start_new_page unless first_page }
3090
3183
  move_cursor_to extent.to.cursor
3091
3184
  end
3092
- @toc_extent = extent
3185
+ extent
3186
+ end
3187
+
3188
+ def get_entries_for_toc node
3189
+ node.sections
3093
3190
  end
3094
3191
 
3095
3192
  # NOTE: num_front_matter_pages not used during a dry run
3096
- def layout_toc doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0
3193
+ def ink_toc doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0
3097
3194
  go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
3098
3195
  start_page_number = page_number
3099
3196
  move_cursor_to start_cursor
3100
3197
  unless (toc_title = doc.attr 'toc-title').nil_or_empty?
3101
3198
  theme_font_cascade [[:heading, level: 2], :toc_title] do
3102
- toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
3103
- layout_general_heading doc, toc_title, align: toc_title_align, level: 2, outdent: true, role: :toctitle
3199
+ toc_title_align = (@theme.toc_title_text_align || @theme.heading_h2_text_align || @theme.heading_text_align || @base_text_align).to_sym
3200
+ ink_general_heading doc, toc_title, align: toc_title_align, level: 2, outdent: true, role: :toctitle
3104
3201
  end
3105
3202
  end
3106
3203
  # QUESTION: should we skip this whole method if num_levels < 0?
@@ -3125,7 +3222,7 @@ module Asciidoctor
3125
3222
  }
3126
3223
  end
3127
3224
  theme_margin :toc, :top
3128
- layout_toc_level doc.sections, num_levels, dot_leader, num_front_matter_pages
3225
+ ink_toc_level (get_entries_for_toc doc), num_levels, dot_leader, num_front_matter_pages
3129
3226
  end
3130
3227
  # NOTE: range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
3131
3228
  toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
@@ -3133,38 +3230,48 @@ module Asciidoctor
3133
3230
  toc_page_numbers
3134
3231
  end
3135
3232
 
3136
- def layout_toc_level sections, num_levels, dot_leader, num_front_matter_pages
3233
+ def ink_toc_level entries, num_levels, dot_leader, num_front_matter_pages
3137
3234
  # NOTE: font options aren't always reliable, so store size separately
3138
3235
  toc_font_info = theme_font :toc do
3139
3236
  { font: font, size: @font_size }
3140
3237
  end
3141
3238
  hanging_indent = @theme.toc_hanging_indent
3142
- sections.each do |sect|
3143
- next if (num_levels_for_sect = (sect.attr 'toclevels', num_levels).to_i) < sect.level
3144
- theme_font :toc, level: (sect.level + 1) do
3145
- sect_title = @text_transform ? (transform_text sect.numbered_title, @text_transform) : sect.numbered_title
3146
- next if sect_title.empty?
3239
+ entries.each do |entry|
3240
+ next if (num_levels_for_entry = (entry.attr 'toclevels', num_levels).to_i) < (entry_level = entry.level + 1).pred
3241
+ theme_font :toc, level: entry_level do
3242
+ next unless (entry_anchor = (entry.attr 'pdf-anchor') || entry.id)
3243
+ entry_title = entry.context == :section ? entry.numbered_title : (entry.title? ? entry.title : (entry.xreftext 'basic'))
3244
+ next if entry_title.empty?
3245
+ entry_title = transform_text entry_title, @text_transform if @text_transform
3147
3246
  pgnum_label_placeholder_width = rendered_width_of_string '0' * @toc_max_pagenum_digits
3148
- # NOTE: only write section title (excluding dots and page number) if this is a dry run
3247
+ # NOTE: only write title (excluding dots and page number) if this is a dry run
3149
3248
  if scratch?
3150
3249
  indent 0, pgnum_label_placeholder_width do
3151
3250
  # NOTE: must wrap title in empty anchor element in case links are styled with different font family / size
3152
- layout_prose sect_title, anchor: true, normalize: false, hanging_indent: hanging_indent, normalize_line_height: true, margin: 0
3251
+ ink_prose entry_title, anchor: true, normalize: false, hanging_indent: hanging_indent, normalize_line_height: true, margin: 0
3153
3252
  end
3154
3253
  else
3155
- physical_pgnum = sect.attr 'pdf-page-start'
3156
- virtual_pgnum = physical_pgnum - num_front_matter_pages
3157
- pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
3254
+ if !(physical_pgnum = entry.attr 'pdf-page-start') &&
3255
+ (target_page_ref = (get_dest entry_anchor)&.first) &&
3256
+ (target_page_idx = state.pages.index {|candidate| candidate.dictionary == target_page_ref })
3257
+ physical_pgnum = target_page_idx + 1
3258
+ end
3259
+ if physical_pgnum
3260
+ virtual_pgnum = physical_pgnum - num_front_matter_pages
3261
+ pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
3262
+ else
3263
+ pgnum_label = '?'
3264
+ end
3158
3265
  start_page_number = page_number
3159
3266
  start_cursor = cursor
3160
3267
  start_dots = nil
3161
- sect_title_inherited = (apply_text_decoration ::Set.new, :toc, sect.level.next).merge anchor: (sect_anchor = sect.attr 'pdf-anchor'), color: @font_color
3268
+ entry_title_inherited = (apply_text_decoration ::Set.new, :toc, entry_level).merge anchor: entry_anchor, color: @font_color
3162
3269
  # NOTE: use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag
3163
- sect_title_fragments = text_formatter.format sect_title, inherited: sect_title_inherited
3270
+ entry_title_fragments = text_formatter.format entry_title, inherited: entry_title_inherited
3164
3271
  line_metrics = calc_line_metrics @base_line_height
3165
3272
  indent 0, pgnum_label_placeholder_width do
3166
- (sect_title_fragments[-1][:callback] ||= []) << (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
3167
- typeset_formatted_text sect_title_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true
3273
+ (entry_title_fragments[-1][:callback] ||= []) << (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
3274
+ typeset_formatted_text entry_title_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true
3168
3275
  start_dots = last_fragment_pos.right + hanging_indent
3169
3276
  last_fragment_cursor = last_fragment_pos.top + line_metrics.padding_top
3170
3277
  start_cursor = last_fragment_cursor if last_fragment_pos.page_number > start_page_number || (start_cursor - last_fragment_cursor) > line_metrics.height
@@ -3172,7 +3279,7 @@ module Asciidoctor
3172
3279
  end_cursor = cursor
3173
3280
  move_cursor_to start_cursor
3174
3281
  # NOTE: we're guaranteed to be on the same page as the final line of the entry
3175
- if dot_leader[:width] > 0 && (dot_leader[:levels].include? sect.level)
3282
+ if dot_leader[:width] > 0 && (dot_leader[:levels].include? entry_level.pred)
3176
3283
  pgnum_label_width = rendered_width_of_string pgnum_label
3177
3284
  pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
3178
3285
  save_font do
@@ -3184,18 +3291,18 @@ module Asciidoctor
3184
3291
  typeset_formatted_text [
3185
3292
  { text: dot_leader[:text] * num_dots, color: dot_leader[:font_color] },
3186
3293
  dot_leader[:spacer],
3187
- ({ text: pgnum_label, anchor: sect_anchor }.merge pgnum_label_font_settings),
3294
+ ({ text: pgnum_label, anchor: entry_anchor }.merge pgnum_label_font_settings),
3188
3295
  ], line_metrics, align: :right
3189
3296
  end
3190
3297
  else
3191
- typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: sect_anchor }], line_metrics, align: :right
3298
+ typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: entry_anchor }], line_metrics, align: :right
3192
3299
  end
3193
3300
  move_cursor_to end_cursor
3194
3301
  end
3195
3302
  end
3196
3303
  indent @theme.toc_indent do
3197
- layout_toc_level sect.sections, num_levels_for_sect, dot_leader, num_front_matter_pages
3198
- end if num_levels_for_sect > sect.level
3304
+ ink_toc_level (get_entries_for_toc entry), num_levels_for_entry, dot_leader, num_front_matter_pages
3305
+ end if num_levels_for_entry >= entry_level
3199
3306
  end
3200
3307
  end
3201
3308
 
@@ -3222,15 +3329,15 @@ module Asciidoctor
3222
3329
  icon_data
3223
3330
  end
3224
3331
 
3225
- # TODO: delegate to layout_page_header and layout_page_footer per page
3226
- def layout_running_content periphery, doc, skip = [1, 1], body_start_page_number = 1
3332
+ # TODO: delegate to ink_page_header and ink_page_footer per page
3333
+ def ink_running_content periphery, doc, skip = [1, 1], body_start_page_number = 1
3227
3334
  skip_pages, skip_pagenums = skip
3228
3335
  # NOTE: find and advance to first non-imported content page to use as model page
3229
- return unless (content_start_page = state.pages[skip_pages..-1].index {|it| !it.imported_page? })
3230
- content_start_page += (skip_pages + 1)
3336
+ return unless (content_start_page_number = state.pages[skip_pages..-1].index {|it| !it.imported_page? })
3337
+ content_start_page_number += (skip_pages + 1)
3231
3338
  num_pages = page_count
3232
3339
  prev_page_number = page_number
3233
- go_to_page content_start_page
3340
+ go_to_page content_start_page_number
3234
3341
 
3235
3342
  # FIXME: probably need to treat doctypes differently
3236
3343
  is_book = doc.doctype == 'book'
@@ -3326,7 +3433,7 @@ module Asciidoctor
3326
3433
  pagenums_enabled = doc.attr? 'pagenums'
3327
3434
  periphery_layout_cache = {}
3328
3435
  # NOTE: this block is invoked during PDF generation, after #write -> #render_file and thus after #convert_document
3329
- repeat (content_start_page..num_pages), dynamic: true do
3436
+ repeat (content_start_page_number..num_pages), dynamic: true do
3330
3437
  pgnum = page_number
3331
3438
  # NOTE: don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
3332
3439
  next if page.imported_page? || (disable_on_pages.include? pgnum)
@@ -3363,7 +3470,7 @@ module Asciidoctor
3363
3470
  theme_font periphery do
3364
3471
  canvas do
3365
3472
  bounding_box [trim_styles[:content_left][side], trim_styles[:top][side]], width: trim_styles[:content_width][side], height: trim_styles[:height] do
3366
- if (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
3473
+ if trim_styles[:column_rule_color] && (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
3367
3474
  trim_column_rule_spacing = trim_styles[:column_rule_spacing]
3368
3475
  else
3369
3476
  trim_column_rule_width = nil
@@ -3448,7 +3555,7 @@ module Asciidoctor
3448
3555
  trim_content_margin_recto = @theme[%(#{periphery}_recto_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
3449
3556
  trim_content_margin_recto = (expand_margin_value trim_content_margin_recto).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_recto[i] - trim_margin_recto[i] : v.to_f }
3450
3557
  if (trim_padding_recto = @theme[%(#{periphery}_recto_padding)] || @theme[%(#{periphery}_padding)])
3451
- trim_padding_recto = (expand_margin_value trim_padding_recto).map.with_index {|v, i| v + trim_content_margin_recto[i] }
3558
+ trim_padding_recto = (expand_padding_value trim_padding_recto).map.with_index {|v, i| v + trim_content_margin_recto[i] }
3452
3559
  else
3453
3560
  trim_padding_recto = trim_content_margin_recto
3454
3561
  end
@@ -3458,7 +3565,7 @@ module Asciidoctor
3458
3565
  trim_content_margin_verso = @theme[%(#{periphery}_verso_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
3459
3566
  trim_content_margin_verso = (expand_margin_value trim_content_margin_verso).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_verso[i] - trim_margin_verso[i] : v.to_f }
3460
3567
  if (trim_padding_verso = @theme[%(#{periphery}_verso_padding)] || @theme[%(#{periphery}_padding)])
3461
- trim_padding_verso = (expand_margin_value trim_padding_verso).map.with_index {|v, i| v + trim_content_margin_verso[i] }
3568
+ trim_padding_verso = (expand_padding_value trim_padding_verso).map.with_index {|v, i| v + trim_content_margin_verso[i] }
3462
3569
  else
3463
3570
  trim_padding_verso = trim_content_margin_verso
3464
3571
  end
@@ -3471,12 +3578,12 @@ module Asciidoctor
3471
3578
  # NOTE: we've already verified this property is set
3472
3579
  height: (trim_height = @theme[%(#{periphery}_height)]),
3473
3580
  bg_color: (resolve_theme_color %(#{periphery}_background_color).to_sym),
3474
- border_color: (trim_border_color = resolve_theme_color %(#{periphery}_border_color).to_sym),
3581
+ border_width: (trim_border_width = @theme[%(#{periphery}_border_width)] || 0),
3582
+ border_color: trim_border_width > 0 ? (resolve_theme_color %(#{periphery}_border_color).to_sym, @theme.base_border_color) : nil,
3475
3583
  border_style: (@theme[%(#{periphery}_border_style)]&.to_sym || :solid),
3476
- border_width: (trim_border_width = trim_border_color ? @theme[%(#{periphery}_border_width)] || @theme.base_border_width : 0),
3477
- column_rule_color: (trim_column_rule_color = resolve_theme_color %(#{periphery}_column_rule_color).to_sym),
3584
+ column_rule_width: (trim_column_rule_width = @theme[%(#{periphery}_column_rule_width)] || 0),
3585
+ column_rule_color: trim_column_rule_width > 0 ? (resolve_theme_color %(#{periphery}_column_rule_color).to_sym) : nil,
3478
3586
  column_rule_style: (@theme[%(#{periphery}_column_rule_style)]&.to_sym || :solid),
3479
- column_rule_width: (trim_column_rule_color ? @theme[%(#{periphery}_column_rule_width)] || 0 : 0),
3480
3587
  column_rule_spacing: (@theme[%(#{periphery}_column_rule_spacing)] || 0),
3481
3588
  valign: valign_offset ? [valign, valign_offset] : valign,
3482
3589
  img_valign: @theme[%(#{periphery}_image_vertical_align)],
@@ -3644,8 +3751,9 @@ module Asciidoctor
3644
3751
  outline.define do
3645
3752
  initial_pagenum = has_front_cover ? 2 : 1
3646
3753
  # FIXME: use sanitize: :plain_text on Document#doctitle once available
3647
- if document.page_count >= initial_pagenum && (doctitle = document.resolve_doctitle doc)
3648
- page title: (document.sanitize doctitle), destination: (document.dest_top initial_pagenum)
3754
+ if document.page_count >= initial_pagenum && (outline_title = doc.attr 'outline-title') &&
3755
+ (outline_title.empty? ? (outline_title = document.resolve_doctitle doc) : outline_title)
3756
+ page title: (document.sanitize outline_title), destination: (document.dest_top initial_pagenum)
3649
3757
  end
3650
3758
  # QUESTION: is there any way to get add_outline_level to invoke in the context of the outline?
3651
3759
  document.add_outline_level self, doc.sections, num_levels, expand_levels
@@ -3707,7 +3815,14 @@ module Asciidoctor
3707
3815
  pdf_doc.render_file target
3708
3816
  # QUESTION: restore attributes first?
3709
3817
  @pdfmark&.generate_file target
3710
- (Optimizer.new @optimize, pdf_doc.min_version).optimize_file target if @optimize
3818
+ if (quality = @optimize)
3819
+ if quality.include? ','
3820
+ quality, compliance = quality.split ',', 2
3821
+ elsif quality.include? '/'
3822
+ quality, compliance = nil, quality
3823
+ end
3824
+ (Optimizer.new quality, pdf_doc.min_version, compliance).optimize_file target
3825
+ end
3711
3826
  to_file = true
3712
3827
  end
3713
3828
  if !ENV['KEEP_ARTIFACTS']
@@ -3759,7 +3874,7 @@ module Asciidoctor
3759
3874
  end
3760
3875
 
3761
3876
  def resolve_text_transform key, use_fallback = true
3762
- if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key.to_s])
3877
+ if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key])
3763
3878
  transform == 'none' ? nil : transform
3764
3879
  elsif use_fallback
3765
3880
  @text_transform
@@ -3768,9 +3883,9 @@ module Asciidoctor
3768
3883
 
3769
3884
  # QUESTION: should we pass a category as an argument?
3770
3885
  # QUESTION: should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
3771
- def resolve_theme_color key, fallback_color = nil
3772
- if (color = @theme[key.to_s]) && color != 'transparent'
3773
- color
3886
+ def resolve_theme_color key, fallback_color = nil, transparent_color = fallback_color
3887
+ if (color = @theme[key])
3888
+ color == 'transparent' ? transparent_color : color
3774
3889
  else
3775
3890
  fallback_color
3776
3891
  end
@@ -3781,7 +3896,7 @@ module Asciidoctor
3781
3896
  end
3782
3897
 
3783
3898
  def theme_fill_and_stroke_bounds category, opts = {}
3784
- fill_and_stroke_bounds opts[:background_color], @theme[%(#{category}_border_color)],
3899
+ fill_and_stroke_bounds opts[:background_color], @theme[%(#{category}_border_color)] || @theme.base_border_color,
3785
3900
  line_width: @theme[%(#{category}_border_width)],
3786
3901
  line_style: (@theme[%(#{category}_border_style)]&.to_sym || :solid),
3787
3902
  radius: @theme[%(#{category}_border_radius)]
@@ -3790,7 +3905,7 @@ module Asciidoctor
3790
3905
  def theme_fill_and_stroke_block category, extent, opts = {}
3791
3906
  node_with_caption = nil unless (node_with_caption = opts[:caption_node])&.title?
3792
3907
  unless extent
3793
- layout_caption node_with_caption, category: category if node_with_caption
3908
+ ink_caption node_with_caption, category: category if node_with_caption
3794
3909
  return
3795
3910
  end
3796
3911
  if (b_width = (opts.key? :border_width) ? opts[:border_width] : @theme[%(#{category}_border_width)])
@@ -3804,14 +3919,12 @@ module Asciidoctor
3804
3919
  bg_color = nil
3805
3920
  end
3806
3921
  unless b_width || bg_color
3807
- layout_caption node_with_caption, category: category if node_with_caption
3922
+ ink_caption node_with_caption, category: category if node_with_caption
3808
3923
  return
3809
3924
  end
3810
- if (b_color = @theme[%(#{category}_border_color)]) == 'transparent'
3811
- b_color = @page_bg_color
3812
- end
3925
+ b_color = resolve_theme_color %(#{category}_border_color).to_sym, @theme.base_border_color, @page_bg_color
3813
3926
  b_radius ||= (@theme[%(#{category}_border_radius)] || 0) + (b_width || 0)
3814
- if b_width && b_color
3927
+ if b_width
3815
3928
  if b_color == @page_bg_color # let page background cut into block background
3816
3929
  b_gap_color, b_shift = @page_bg_color, (b_width * 0.5)
3817
3930
  elsif (b_gap_color = bg_color) && b_gap_color != b_color
@@ -3822,7 +3935,7 @@ module Asciidoctor
3822
3935
  else # let page background cut into block background; guarantees b_width is set
3823
3936
  b_shift, b_gap_color = (b_width ||= 0.5) * 0.5, @page_bg_color
3824
3937
  end
3825
- layout_caption node_with_caption, category: category if node_with_caption
3938
+ ink_caption node_with_caption, category: category if node_with_caption
3826
3939
  extent.from.page += 1 unless extent.from.page == page_number # sanity check
3827
3940
  float do
3828
3941
  extent.each_page do |first_page, last_page|
@@ -3959,9 +4072,7 @@ module Asciidoctor
3959
4072
  # NOTE: it also removes zero-width spaces
3960
4073
  arranger.finalize_line
3961
4074
  actual_width = width_of_fragments arranger.fragments
3962
- unless ::Array === (padding = @theme[%(#{category}_padding)])
3963
- padding = ::Array.new 4, padding
3964
- end
4075
+ padding = expand_padding_value @theme[%(#{category}_padding)]
3965
4076
  if actual_width > (available_width = bounds.width - padding[3].to_f - padding[1].to_f)
3966
4077
  adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate 4
3967
4078
  if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
@@ -4206,7 +4317,7 @@ module Asciidoctor
4206
4317
  (align_role.slice 5, align_role.length).to_sym
4207
4318
  elsif use_theme
4208
4319
  roles.reverse.each do |role|
4209
- if (align = @theme[%(role_#{role}_align)])
4320
+ if (align = @theme[%(role_#{role}_text_align)])
4210
4321
  return align.to_sym
4211
4322
  end
4212
4323
  end
@@ -4542,6 +4653,35 @@ module Asciidoctor
4542
4653
  end
4543
4654
  end
4544
4655
 
4656
+ # Deprecated method names
4657
+ alias layout_footnotes ink_footnotes
4658
+ alias layout_title_page ink_title_page
4659
+ alias layout_cover_page ink_cover_page
4660
+ alias layout_chapter_title ink_chapter_title
4661
+ alias layout_part_title ink_part_title
4662
+ alias layout_general_heading ink_general_heading
4663
+ alias layout_heading ink_heading
4664
+ alias layout_prose ink_prose
4665
+ alias layout_caption ink_caption
4666
+ alias layout_table_caption ink_table_caption
4667
+ alias layout_toc ink_toc
4668
+ alias layout_toc_level ink_toc_level
4669
+ alias layout_running_content ink_running_content
4670
+
4671
+ # intercepts "class CustomPDFConverter < (Asciidoctor::Converter.for 'pdf')"
4672
+ def self.method_added method
4673
+ if (method_name = method.to_s).start_with? 'layout_'
4674
+ alias_method %(ink_#{method_name.slice 7, method_name.length}).to_sym, method
4675
+ end
4676
+ end
4677
+
4678
+ # intercepts "(Asciidoctor::Converter.for 'pdf').prepend CustomConverterExtensions"
4679
+ def self.prepend *mods
4680
+ super
4681
+ mods.each {|mod| (mod.instance_methods false).each {|method| method_added method } }
4682
+ self
4683
+ end
4684
+
4545
4685
  private
4546
4686
 
4547
4687
  def add_link_to_image uri, image_info, image_opts