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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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