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

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +185 -83
  3. data/README.adoc +31 -126
  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 +10 -20
  21. data/data/themes/default-for-print-theme.yml +4 -4
  22. data/data/themes/default-for-print-with-fallback-font-theme.yml +2 -3
  23. data/data/themes/default-for-print-with-font-fallbacks-theme.yml +3 -0
  24. data/data/themes/{sans-with-fallback-font-theme.yml → default-sans-theme.yml} +1 -1
  25. data/data/themes/default-sans-with-font-fallbacks-theme.yml +3 -0
  26. data/data/themes/default-theme.yml +5 -4
  27. data/data/themes/default-with-fallback-font-theme.yml +2 -9
  28. data/data/themes/default-with-font-fallbacks-theme.yml +9 -0
  29. data/docs/theming-guide.adoc +5 -6050
  30. data/lib/asciidoctor/pdf/converter.rb +505 -294
  31. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +8 -0
  32. data/lib/asciidoctor/pdf/ext/prawn/document/column_box.rb +16 -0
  33. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +110 -58
  34. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +14 -0
  35. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/indented_paragraph_wrap.rb +39 -0
  36. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +3 -10
  37. data/lib/asciidoctor/pdf/ext/prawn.rb +2 -0
  38. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +7 -2
  39. data/lib/asciidoctor/pdf/nopngmagick.rb +3 -0
  40. data/lib/asciidoctor/pdf/optimizer.rb +12 -5
  41. data/lib/asciidoctor/pdf/text_transformer.rb +14 -0
  42. data/lib/asciidoctor/pdf/theme_loader.rb +9 -3
  43. data/lib/asciidoctor/pdf/version.rb +1 -1
  44. metadata +9 -3
@@ -52,6 +52,7 @@ module Asciidoctor
52
52
  TextAlignmentRoles = %w(text-justify text-left text-center text-right)
53
53
  TextDecorationStyleTable = { 'underline' => :underline, 'line-through' => :strikethrough }
54
54
  FontKerningTable = { 'normal' => true, 'none' => false }
55
+ BlockFloatNames = %w(left right)
55
56
  BlockAlignmentNames = %w(left center right)
56
57
  (AlignmentTable = { '<' => :left, '=' => :center, '>' => :right }).default = :left
57
58
  ColumnPositions = [:left, :center, :right]
@@ -139,6 +140,7 @@ module Asciidoctor
139
140
  # NOTE: enabling data-uri forces Asciidoctor Diagram to produce absolute image paths
140
141
  doc.attributes['data-uri'] = (doc.instance_variable_get :@attribute_overrides)['data-uri'] = ''
141
142
  end
143
+ @label = :primary
142
144
  @initial_instance_variables = [:@initial_instance_variables] + instance_variables
143
145
  end
144
146
 
@@ -168,7 +170,7 @@ module Asciidoctor
168
170
  if (bottom_gutter = @bottom_gutters[-1][node])
169
171
  prose_opts[:bottom_gutter] = bottom_gutter
170
172
  end
171
- inscribe_prose string, prose_opts
173
+ ink_prose string, prose_opts
172
174
  end
173
175
  ensure
174
176
  node.document.instance_variable_set :@converter, prev_converter if prev_converter
@@ -181,8 +183,9 @@ module Asciidoctor
181
183
  def convert_document doc
182
184
  doc.promote_preface_block
183
185
  init_pdf doc
184
- # set default value for outline, pagenums, and show-link-uri if not otherwise set
186
+ # set default value for outline, outline-title, and pagenums attributes if not otherwise set
185
187
  doc.attributes['outline'] = '' unless (doc.attribute_locked? 'outline') || ((doc.instance_variable_get :@attributes_modified).include? 'outline')
188
+ doc.attributes['outline-title'] = '' unless (doc.attribute_locked? 'outline-title') || ((doc.instance_variable_get :@attributes_modified).include? 'outline-title')
186
189
  doc.attributes['pagenums'] = '' unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
187
190
  #assign_missing_section_ids doc
188
191
 
@@ -190,22 +193,22 @@ module Asciidoctor
190
193
 
191
194
  marked_page_number = page_number
192
195
  # NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF
193
- inscribe_cover_page doc, :front
196
+ ink_cover_page doc, :front
194
197
  has_front_cover = page_number > marked_page_number
195
- has_title_page = inscribe_title_page doc if (title_page_on = doc.doctype == 'book' || (doc.attr? 'title-page'))
198
+ has_title_page = ink_title_page doc if (title_page_on = doc.doctype == 'book' || (doc.attr? 'title-page'))
196
199
 
197
200
  @page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress' && page_number == 0
198
201
 
199
202
  start_new_page unless page&.empty? # rubocop:disable Lint/SafeNavigationWithEmpty
200
203
 
201
204
  # NOTE: the base font must be set before any content is written to the main or scratch document
202
- # this method is called inside inscribe_title_page if the title page is active
205
+ # this method is called inside ink_title_page if the title page is active
203
206
  font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style unless has_title_page
204
207
 
205
208
  unless title_page_on
206
209
  body_start_page_number = page_number
207
210
  theme_font :heading, level: 1 do
208
- inscribe_general_heading doc, doc.doctitle, align: (@theme.heading_h1_text_align&.to_sym || :center), level: 1, role: :doctitle
211
+ ink_general_heading doc, doc.doctitle, align: (@theme.heading_h1_text_align&.to_sym || :center), level: 1, role: :doctitle
209
212
  end if doc.header? && !doc.notitle
210
213
  end
211
214
 
@@ -216,7 +219,7 @@ module Asciidoctor
216
219
  if (insert_toc = (doc.attr? 'toc') && !((toc_placement = doc.attr 'toc-placement') == 'macro' || toc_placement == 'preamble') && doc.sections?)
217
220
  start_new_page if @ppbook && verso_page?
218
221
  add_dest_for_block doc, id: 'toc', y: (at_page_top? ? page_height : nil)
219
- allocate_toc doc, toc_num_levels, cursor, title_page_on
222
+ @toc_extent = allocate_toc doc, toc_num_levels, cursor, title_page_on
220
223
  else
221
224
  @toc_extent = nil
222
225
  end
@@ -301,26 +304,26 @@ module Asciidoctor
301
304
  traverse doc
302
305
 
303
306
  # NOTE: for a book, these are leftover footnotes; for an article this is everything
304
- outdent_section { inscribe_footnotes doc }
307
+ outdent_section { ink_footnotes doc }
305
308
 
306
- if @toc_extent
309
+ if (toc_extent = @toc_extent)
307
310
  if title_page_on && !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'
311
+ num_front_matter_pages[0] = toc_extent.to.page if @theme.running_content_start_at == 'after-toc'
312
+ num_front_matter_pages[1] = toc_extent.to.page if @theme.page_numbering_start_at == 'after-toc'
310
313
  end
311
- toc_page_nums = inscribe_toc doc, toc_num_levels, @toc_extent.from.page, @toc_extent.from.cursor, num_front_matter_pages[1]
314
+ toc_page_nums = ink_toc doc, toc_num_levels, toc_extent.from.page, toc_extent.from.cursor, num_front_matter_pages[1]
312
315
  else
313
316
  toc_page_nums = []
314
317
  end
315
318
 
316
319
  # NOTE: delete orphaned page (a page was created but there was no additional content)
317
320
  # QUESTION: should we delete page if document is empty? (leaving no pages?)
318
- delete_page if page_count > 1 && page.empty?
321
+ delete_current_page if page_count > 1 && page.empty?
319
322
  end
320
323
 
321
324
  unless page_count < body_start_page_number
322
- inscribe_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
- inscribe_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
325
+ 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
326
+ 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
327
  end
325
328
 
326
329
  add_outline doc, (doc.attr 'outlinelevels', toc_num_levels), toc_page_nums, num_front_matter_pages[1], has_front_cover
@@ -337,7 +340,7 @@ module Asciidoctor
337
340
  catalog.data[:ViewerPreferences] = { DisplayDocTitle: true }
338
341
 
339
342
  stamp_foreground_image doc, has_front_cover
340
- inscribe_cover_page doc, :back
343
+ ink_cover_page doc, :back
341
344
  add_dest_for_top doc
342
345
  nil
343
346
  end
@@ -461,7 +464,6 @@ module Asciidoctor
461
464
  end
462
465
 
463
466
  def prepare_theme theme
464
- theme.base_border_width || 0
465
467
  theme.base_font_color ||= '000000'
466
468
  theme.base_font_size ||= 12
467
469
  theme.base_font_style = theme.base_font_style&.to_sym || :normal
@@ -478,12 +480,16 @@ module Asciidoctor
478
480
  theme.list_item_spacing ||= 0
479
481
  theme.description_list_term_spacing ||= 0
480
482
  theme.description_list_description_indent ||= 0
483
+ theme.table_border_color ||= (theme.base_border_color || '000000')
484
+ theme.table_border_width ||= 0.5
485
+ theme.thematic_break_border_color ||= (theme.base_border_color || '000000')
481
486
  theme.image_border_width ||= 0
482
487
  theme.code_linenum_font_color ||= '999999'
483
488
  theme.callout_list_margin_top_after_code ||= 0
484
489
  theme.role_unresolved_font_color ||= 'FF0000'
485
- theme.index_columns ||= 2
486
490
  theme.footnotes_item_spacing ||= 0
491
+ theme.index_columns ||= 2
492
+ theme.index_column_gap ||= theme.base_font_size
487
493
  theme.kbd_separator ||= '+'
488
494
  theme.title_page_authors_delimiter ||= ', '
489
495
  theme.title_page_revision_delimiter ||= ', '
@@ -661,24 +667,24 @@ module Asciidoctor
661
667
  title, _, subtitle = title.rpartition sep
662
668
  title = %(#{title}\n<em class="subtitle">#{subtitle}</em>)
663
669
  end
664
- hlevel = sect.level + 1
670
+ hlevel = sect.level.next
665
671
  align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
666
672
  chapterlike = !(part = sectname == 'part') && (sectname == 'chapter' || (sect.document.doctype == 'book' && sect.level == 1))
667
673
  hidden = sect.option? 'notitle'
668
674
  hopts = { align: align, level: hlevel, part: part, chapterlike: chapterlike, outdent: !(part || chapterlike) }
669
675
  if part
670
676
  unless @theme.heading_part_break_before == 'auto'
671
- start_new = true
677
+ started_new = true
672
678
  start_new_part sect
673
679
  end
674
680
  elsif chapterlike
675
681
  if @theme.heading_chapter_break_before != 'auto' ||
676
682
  (@theme.heading_part_break_after == 'always' && sect == sect.parent.sections[0])
677
- start_new = true
683
+ started_new = true
678
684
  start_new_chapter sect
679
685
  end
680
686
  end
681
- arrange_section sect, title, hopts unless hidden || start_new || at_page_top?
687
+ arrange_heading sect, title, hopts unless hidden || started_new || at_page_top? || !sect.blocks?
682
688
  # QUESTION: should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
683
689
  sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
684
690
  # QUESTION: should we just assign the section this generated id?
@@ -687,11 +693,11 @@ module Asciidoctor
687
693
  add_dest_for_block sect, id: sect_anchor, y: (at_page_top? ? page_height : nil)
688
694
  theme_font :heading, level: hlevel do
689
695
  if part
690
- inscribe_part_title sect, title, hopts
696
+ ink_part_title sect, title, hopts
691
697
  elsif chapterlike
692
- inscribe_chapter_title sect, title, hopts
698
+ ink_chapter_title sect, title, hopts
693
699
  else
694
- inscribe_general_heading sect, title, hopts
700
+ ink_general_heading sect, title, hopts
695
701
  end
696
702
  end unless hidden
697
703
 
@@ -700,7 +706,7 @@ module Asciidoctor
700
706
  else
701
707
  traverse sect
702
708
  end
703
- outdent_section { inscribe_footnotes sect } if chapterlike
709
+ outdent_section { ink_footnotes sect } if chapterlike
704
710
  sect.set_attr 'pdf-page-end', page_number
705
711
  end
706
712
 
@@ -721,7 +727,7 @@ module Asciidoctor
721
727
  end
722
728
 
723
729
  # QUESTION: if a footnote ref appears in a separate chapter, should the footnote def be duplicated?
724
- def inscribe_footnotes node
730
+ def ink_footnotes node
725
731
  return if (fns = (doc = node.document).footnotes - @rendered_footnotes).empty?
726
732
  theme_margin :block, :bottom if node.context == :document || node == node.document.blocks[-1]
727
733
  theme_margin :footnotes, :top
@@ -730,7 +736,7 @@ module Asciidoctor
730
736
  move_down delta
731
737
  end
732
738
  theme_font :footnotes do
733
- (title = doc.attr 'footnotes-title') && (inscribe_caption title, category: :footnotes)
739
+ (title = doc.attr 'footnotes-title') && (ink_caption title, category: :footnotes)
734
740
  item_spacing = @theme.footnotes_item_spacing
735
741
  index_offset = @rendered_footnotes.length
736
742
  sect_xreftext = node.context == :section && (node.xreftext node.document.attr 'xrefstyle')
@@ -740,7 +746,7 @@ module Asciidoctor
740
746
  fn.singleton_class.send :attr_accessor, :label unless fn.respond_to? :label=
741
747
  fn.label = %(#{label} - #{sect_xreftext})
742
748
  end
743
- inscribe_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
749
+ ink_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
744
750
  end
745
751
  @rendered_footnotes += fns if extent
746
752
  end
@@ -749,14 +755,17 @@ module Asciidoctor
749
755
  end
750
756
 
751
757
  def convert_floating_title node
752
- add_dest_for_block node if node.id
758
+ title = node.title
753
759
  hlevel = node.level.next
754
- unless (align = resolve_alignment_from_role node.roles)
760
+ unless (align = resolve_text_align_from_role node.roles)
755
761
  align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
756
762
  end
763
+ hopts = { align: align, level: hlevel, outdent: (parent = node.parent).context == :section }
764
+ arrange_heading node, title, hopts unless at_page_top? || node == parent.blocks[-1]
765
+ add_dest_for_block node if node.id
757
766
  # QUESTION: should we decouple styles from section titles?
758
767
  theme_font :heading, level: hlevel do
759
- inscribe_general_heading node, node.title, align: align, level: hlevel, outdent: (node.parent.context == :section)
768
+ ink_general_heading node, title, hopts
760
769
  end
761
770
  end
762
771
 
@@ -765,7 +774,7 @@ module Asciidoctor
765
774
  outdent_section do
766
775
  pad_box @theme.abstract_padding do
767
776
  theme_font :abstract_title do
768
- inscribe_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)
777
+ 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)
769
778
  end if node.title?
770
779
  theme_font :abstract do
771
780
  prose_opts = { align: (@theme.abstract_text_align || @base_text_align).to_sym, hyphenate: true }
@@ -774,11 +783,21 @@ module Asciidoctor
774
783
  end
775
784
  # FIXME: allow theme to control more first line options
776
785
  if (line1_font_style = @theme.abstract_first_line_font_style&.to_sym) && line1_font_style != font_style
777
- first_line_options = { styles: line1_font_style == :normal ? [] : [font_style, line1_font_style] }
786
+ case line1_font_style
787
+ when :normal
788
+ first_line_options = { styles: [] }
789
+ when :normal_italic
790
+ first_line_options = { styles: [:italic] }
791
+ else
792
+ first_line_options = { styles: [font_style, line1_font_style] }
793
+ end
778
794
  end
779
795
  if (line1_font_color = @theme.abstract_first_line_font_color)
780
796
  (first_line_options ||= {})[:color] = line1_font_color
781
797
  end
798
+ if (line1_text_transform = @theme.abstract_first_line_text_transform)
799
+ (first_line_options ||= {})[:text_transform] = line1_text_transform
800
+ end
782
801
  prose_opts[:first_line_options] = first_line_options if first_line_options
783
802
  # FIXME: make this cleaner!!
784
803
  if node.blocks?
@@ -787,7 +806,7 @@ module Asciidoctor
787
806
  if child.context == :paragraph
788
807
  child.document.playback_attributes child.attributes
789
808
  prose_opts[:margin_bottom] = 0 if child == last_block
790
- inscribe_prose child.content, ((align = resolve_alignment_from_role child.roles) ? (prose_opts.merge align: align) : prose_opts.dup)
809
+ ink_prose child.content, ((align = resolve_text_align_from_role child.roles) ? (prose_opts.merge align: align) : prose_opts.dup)
791
810
  prose_opts.delete :first_line_options
792
811
  prose_opts.delete :margin_bottom
793
812
  else
@@ -796,10 +815,10 @@ module Asciidoctor
796
815
  end
797
816
  end
798
817
  elsif node.content_model != :compound && (string = node.content)
799
- if (align = resolve_alignment_from_role node.roles)
818
+ if (align = resolve_text_align_from_role node.roles)
800
819
  prose_opts[:align] = align
801
820
  end
802
- inscribe_prose string, (prose_opts.merge margin_bottom: 0)
821
+ ink_prose string, (prose_opts.merge margin_bottom: 0)
803
822
  end
804
823
  end
805
824
  end
@@ -820,39 +839,39 @@ module Asciidoctor
820
839
 
821
840
  def convert_paragraph node
822
841
  add_dest_for_block node if node.id
842
+
823
843
  prose_opts = { margin_bottom: 0, hyphenate: true }
824
- if (align = resolve_alignment_from_role (roles = node.roles), use_theme: true)
844
+ if (align = resolve_text_align_from_role (roles = node.roles), query_theme: true, remove_predefined: true)
825
845
  prose_opts[:align] = align
826
- roles -= TextAlignmentRoles
827
846
  end
828
-
847
+ role_keys = roles.map {|role| %(role_#{role}).to_sym } unless roles.empty?
829
848
  if (text_indent = @theme.prose_text_indent) > 0 ||
830
849
  ((text_indent = @theme.prose_text_indent_inner) > 0 &&
831
850
  (self_idx = (siblings = node.parent.blocks).index node) > 0 && siblings[self_idx - 1].context == :paragraph)
832
851
  prose_opts[:indent_paragraphs] = text_indent
833
852
  end
834
-
835
- # TODO: check if we're within one line of the bottom of the page
836
- # and advance to the next page if so (similar to logic for section titles)
837
- inscribe_caption node, labeled: false if node.title?
838
-
839
853
  if (bottom_gutter = @bottom_gutters[-1][node])
840
854
  prose_opts[:bottom_gutter] = bottom_gutter
841
855
  end
842
856
 
843
- if roles.empty?
844
- inscribe_prose node.content, prose_opts
845
- else
846
- theme_font_cascade (roles.map {|role| %(role_#{role}).to_sym }) do
847
- inscribe_prose node.content, prose_opts
857
+ block_next = next_enclosed_block node
858
+
859
+ insert_margin_bottom = proc do
860
+ if (margin_inner_val = @theme.prose_margin_inner) && block_next&.context == :paragraph
861
+ margin_bottom margin_inner_val
862
+ else
863
+ theme_margin :prose, :bottom, block_next
848
864
  end
849
865
  end
850
866
 
851
- block_next = next_enclosed_block node
852
- if (margin_inner_val = @theme.prose_margin_inner) && block_next&.context == :paragraph
853
- margin_bottom margin_inner_val
867
+ if (float_box = (@float_box ||= nil))
868
+ ink_paragraph_in_float_box node, float_box, prose_opts, role_keys, block_next, insert_margin_bottom
854
869
  else
855
- theme_margin :prose, :bottom, block_next
870
+ # TODO: check if we're within one line of the bottom of the page
871
+ # and advance to the next page if so (similar to logic for section titles)
872
+ ink_caption node, labeled: false if node.title?
873
+ role_keys ? theme_font_cascade(role_keys) { ink_prose node.content, prose_opts } : (ink_prose node.content, prose_opts)
874
+ insert_margin_bottom.call
856
875
  end
857
876
  end
858
877
 
@@ -892,20 +911,16 @@ module Asciidoctor
892
911
  label_width = label_min_width if label_min_width && label_min_width > label_width
893
912
  end
894
913
  end
895
- unless ::Array === (cpad = @theme.admonition_padding || 0)
896
- cpad = ::Array.new 4, cpad
897
- end
898
- unless ::Array === (lpad = @theme.admonition_label_padding || cpad)
899
- lpad = ::Array.new 4, lpad
900
- end
914
+ cpad = expand_padding_value @theme.admonition_padding
915
+ lpad = (lpad = @theme.admonition_label_padding) ? (expand_padding_value lpad) : cpad
901
916
  arrange_block node do |extent|
902
917
  add_dest_for_block node if node.id
903
918
  theme_fill_and_stroke_block :admonition, extent if extent
904
919
  pad_box [0, cpad[1], 0, lpad[3]] do
905
920
  if extent
906
921
  label_height = extent.single_page_height || cursor
907
- if (rule_color = @theme.admonition_column_rule_color) &&
908
- (rule_width = @theme.admonition_column_rule_width || @theme.base_border_width) && rule_width > 0
922
+ if (rule_width = @theme.admonition_column_rule_width || 0) > 0 &&
923
+ (rule_color = @theme.admonition_column_rule_color || @theme.base_border_color)
909
924
  rule_style = @theme.admonition_column_rule_style&.to_sym || :solid
910
925
  float do
911
926
  extent.each_page do |first_page, last_page|
@@ -996,7 +1011,7 @@ module Asciidoctor
996
1011
  end
997
1012
  end
998
1013
  @text_transform = nil # already applied to label
999
- inscribe_prose label_text,
1014
+ ink_prose label_text,
1000
1015
  align: label_align,
1001
1016
  valign: label_valign,
1002
1017
  line_height: 1,
@@ -1010,7 +1025,7 @@ module Asciidoctor
1010
1025
  end
1011
1026
  end
1012
1027
  pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]], node do
1013
- inscribe_caption node, category: :admonition, labeled: false if node.title?
1028
+ ink_caption node, category: :admonition, labeled: false if node.title?
1014
1029
  theme_font :admonition do
1015
1030
  traverse node
1016
1031
  end
@@ -1044,13 +1059,13 @@ module Asciidoctor
1044
1059
  arrange_block node do
1045
1060
  add_dest_for_block node if id
1046
1061
  tare_first_page_content_stream do
1047
- node.context == :example ? (inscribe_caption %(\u25bc #{node.title})) : (inscribe_caption node, labeled: false)
1062
+ node.context == :example ? (ink_caption %(\u25bc #{node.title})) : (ink_caption node, labeled: false)
1048
1063
  end if has_title
1049
1064
  traverse node
1050
1065
  end
1051
1066
  else
1052
1067
  add_dest_for_block node if id
1053
- node.context == :example ? (inscribe_caption %(\u25bc #{node.title})) : (inscribe_caption node, labeled: false) if has_title
1068
+ node.context == :example ? (ink_caption %(\u25bc #{node.title})) : (ink_caption node, labeled: false) if has_title
1054
1069
  traverse node
1055
1070
  end
1056
1071
  end
@@ -1059,7 +1074,7 @@ module Asciidoctor
1059
1074
  category = node.context == :quote ? :quote : :verse
1060
1075
  # NOTE: b_width and b_left_width are mutually exclusive
1061
1076
  if (b_left_width = @theme[%(#{category}_border_left_width)]) && b_left_width > 0
1062
- b_color = @theme[%(#{category}_border_color)]
1077
+ b_color = @theme[%(#{category}_border_color)] || @theme.base_border_color
1063
1078
  else
1064
1079
  b_left_width = nil
1065
1080
  b_width = nil if (b_width = @theme[%(#{category}_border_width)]) == 0
@@ -1094,7 +1109,7 @@ module Asciidoctor
1094
1109
  traverse node
1095
1110
  else # :verse
1096
1111
  content = guard_indentation node.content
1097
- inscribe_prose content,
1112
+ ink_prose content,
1098
1113
  normalize: false,
1099
1114
  align: :left,
1100
1115
  hyphenate: true,
@@ -1106,7 +1121,7 @@ module Asciidoctor
1106
1121
  margin_bottom @theme.block_margin_bottom
1107
1122
  theme_font %(#{category}_cite) do
1108
1123
  attribution_parts = citetitle ? [attribution, citetitle] : [attribution]
1109
- inscribe_prose %(#{EmDash} #{attribution_parts.join ', '}), align: :left, normalize: false, margin_bottom: 0
1124
+ ink_prose %(#{EmDash} #{attribution_parts.join ', '}), align: :left, normalize: false, margin_bottom: 0
1110
1125
  end
1111
1126
  end
1112
1127
  end
@@ -1122,9 +1137,11 @@ module Asciidoctor
1122
1137
  add_dest_for_block node if node.id
1123
1138
  theme_fill_and_stroke_block :sidebar, extent if extent
1124
1139
  pad_box @theme.sidebar_padding, node do
1125
- theme_font :sidebar_title do
1126
- # QUESTION: should we allow margins of sidebar title to be customized?
1127
- inscribe_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)
1140
+ tare_first_page_content_stream do
1141
+ theme_font :sidebar_title do
1142
+ # QUESTION: should we allow margins of sidebar title to be customized?
1143
+ 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)
1144
+ end
1128
1145
  end if node.title?
1129
1146
  theme_font :sidebar do
1130
1147
  traverse node
@@ -1135,13 +1152,15 @@ module Asciidoctor
1135
1152
  end
1136
1153
 
1137
1154
  def convert_colist node
1138
- margin_top @theme.callout_list_margin_top_after_code if !at_page_top? && ([:listing, :literal].include? node.parent.blocks[(node.parent.blocks.index node) - 1].context)
1155
+ unless at_page_top? || (self_idx = (siblings = node.parent.blocks).index node) == 0 || !([:listing, :literal].include? siblings[self_idx - 1].context)
1156
+ margin_top @theme.callout_list_margin_top_after_code
1157
+ end
1139
1158
  add_dest_for_block node if node.id
1140
1159
  @list_numerals << 1
1141
1160
  last_item = node.items[-1]
1142
1161
  item_spacing = @theme.callout_list_item_spacing || @theme.list_item_spacing
1143
1162
  item_opts = { margin_bottom: item_spacing, normalize_line_height: true }
1144
- if (item_align = (resolve_alignment_from_role node.roles) || @theme.list_text_align&.to_sym)
1163
+ if (item_align = (resolve_text_align_from_role node.roles) || @theme.list_text_align&.to_sym)
1145
1164
  item_opts[:align] = item_align
1146
1165
  end
1147
1166
  theme_font :callout_list do
@@ -1163,7 +1182,7 @@ module Asciidoctor
1163
1182
  marker_width = rendered_width_of_string %(#{marker = conum_glyph index}x)
1164
1183
  float do
1165
1184
  bounding_box [0, cursor], width: marker_width do
1166
- inscribe_prose marker, align: :center, inline_format: false, margin: 0
1185
+ ink_prose marker, align: :center, inline_format: false, margin: 0
1167
1186
  end
1168
1187
  end
1169
1188
  end
@@ -1247,7 +1266,7 @@ module Asciidoctor
1247
1266
  max_term_width += (term_padding[1] + term_padding[3])
1248
1267
  term_column_width = [max_term_width, bounds.width * 0.5].min
1249
1268
  table table_data, position: :left, cell_style: { border_width: 0 }, column_widths: [term_column_width] do
1250
- @pdf.inscribe_table_caption node if node.title?
1269
+ @pdf.ink_table_caption node if node.title?
1251
1270
  end
1252
1271
  theme_margin :prose, :bottom, (next_enclosed_block actual_node) #unless actual_node.nested?
1253
1272
  when 'qanda'
@@ -1257,7 +1276,7 @@ module Asciidoctor
1257
1276
  else
1258
1277
  # TODO: check if we're within one line of the bottom of the page
1259
1278
  # and advance to the next page if so (similar to logic for section titles)
1260
- inscribe_caption node, category: :description_list, labeled: false if node.title?
1279
+ ink_caption node, category: :description_list, labeled: false if node.title?
1261
1280
 
1262
1281
  term_spacing = @theme.description_list_term_spacing
1263
1282
  term_height = theme_font(:description_list_term) { height_of_typeset_text 'A' }
@@ -1269,8 +1288,8 @@ module Asciidoctor
1269
1288
  term_font_styles = nil
1270
1289
  end
1271
1290
  terms.each_with_index do |term, idx|
1272
- # QUESTION: should we pass down styles in other calls to inscribe_prose
1273
- inscribe_prose term.text, margin_top: (idx > 0 ? term_spacing : 0), margin_bottom: 0, align: :left, normalize_line_height: true, styles: term_font_styles
1291
+ # QUESTION: should we pass down styles in other calls to ink_prose
1292
+ ink_prose term.text, margin_top: (idx > 0 ? term_spacing : 0), margin_bottom: 0, align: :left, normalize_line_height: true, styles: term_font_styles
1274
1293
  end
1275
1294
  end
1276
1295
  indent @theme.description_list_description_indent do
@@ -1357,10 +1376,10 @@ module Asciidoctor
1357
1376
  def convert_list node
1358
1377
  # TODO: check if we're within one line of the bottom of the page
1359
1378
  # and advance to the next page if so (similar to logic for section titles)
1360
- inscribe_caption node, category: :list, labeled: false if node.title?
1379
+ ink_caption node, category: :list, labeled: false if node.title?
1361
1380
 
1362
1381
  opts = {}
1363
- if (align = resolve_alignment_from_role node.roles)
1382
+ if (align = resolve_text_align_from_role node.roles)
1364
1383
  opts[:align] = align
1365
1384
  elsif node.style == 'bibliography'
1366
1385
  opts[:align] = :left
@@ -1454,7 +1473,7 @@ module Asciidoctor
1454
1473
  float do
1455
1474
  advance_page if @media == 'prepress' && cursor < marker_height
1456
1475
  flow_bounding_box position: start_position, width: marker_width do
1457
- inscribe_prose marker,
1476
+ ink_prose marker,
1458
1477
  align: :right,
1459
1478
  character_spacing: -0.5,
1460
1479
  color: marker_style[:font_color],
@@ -1482,16 +1501,16 @@ module Asciidoctor
1482
1501
  def traverse_list_item node, list_type, opts = {}
1483
1502
  if list_type == :dlist # qanda
1484
1503
  terms, desc = node
1485
- terms.each {|term| inscribe_prose %(<em>#{term.text}</em>), (opts.merge margin_bottom: @theme.description_list_term_spacing) }
1504
+ terms.each {|term| ink_prose %(<em>#{term.text}</em>), (opts.merge margin_bottom: @theme.description_list_term_spacing) }
1486
1505
  if desc
1487
- inscribe_prose desc.text, (opts.merge hyphenate: true) if desc.text?
1506
+ ink_prose desc.text, (opts.merge hyphenate: true) if desc.text?
1488
1507
  traverse desc
1489
1508
  end
1490
1509
  else
1491
1510
  if (primary_text = node.text).nil_or_empty?
1492
- inscribe_prose DummyText, opts unless node.blocks?
1511
+ ink_prose DummyText, opts unless node.blocks?
1493
1512
  else
1494
- inscribe_prose primary_text, (opts.merge hyphenate: true)
1513
+ ink_prose primary_text, (opts.merge hyphenate: true)
1495
1514
  end
1496
1515
  traverse node
1497
1516
  end
@@ -1553,22 +1572,29 @@ module Asciidoctor
1553
1572
 
1554
1573
  return on_image_error :missing, node, target, opts unless image_path
1555
1574
 
1556
- alignment = (alignment = node.attr 'align') ?
1557
- ((BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left) :
1558
- (resolve_alignment_from_role node.roles) || @theme.image_align&.to_sym || :left
1575
+ if (float_to = node.attr 'float') && ((BlockFloatNames.include? float_to) ? float_to : (float_to = nil))
1576
+ alignment = float_to.to_sym
1577
+ elsif (alignment = node.attr 'align')
1578
+ alignment = (BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left
1579
+ else
1580
+ alignment = (resolve_text_align_from_role node.roles) || @theme.image_align&.to_sym || :left
1581
+ end
1559
1582
  # TODO: support cover (aka canvas) image layout using "canvas" (or "cover") role
1560
1583
  width = resolve_explicit_width node.attributes, bounds_width: (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true
1561
1584
  # TODO: add `to_pt page_width` method to ViewportWidth type
1562
1585
  width = (width.to_f / 100) * page_width if ViewportWidth === width
1563
1586
 
1587
+ caption_end = @theme.image_caption_end&.to_sym || :bottom
1588
+ caption_max_width = @theme.image_caption_max_width
1589
+ caption_max_width = 'fit-content' if float_to && !(caption_max_width&.start_with? 'fit-content')
1564
1590
  # NOTE: if width is not set explicitly and max-width is fit-content, caption height may not be accurate
1565
- caption_h = node.title? ? (inscribe_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
1591
+ 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
1566
1592
 
1567
1593
  align_to_page = node.option? 'align-to-page'
1568
1594
  pinned = opts[:pinned]
1569
1595
 
1570
1596
  begin
1571
- rendered_w = nil
1597
+ rendered_h = rendered_w = nil
1572
1598
  span_page_width_if align_to_page do
1573
1599
  if image_format == 'svg'
1574
1600
  if ::Base64 === image_path
@@ -1598,14 +1624,14 @@ module Asciidoctor
1598
1624
  end
1599
1625
  rendered_w = (svg_obj.resize height: (rendered_h = available_h)).output_width if rendered_h > available_h
1600
1626
  end
1601
- image_y = y
1602
- image_cursor = cursor
1603
1627
  add_dest_for_block node if node.id
1604
1628
  # NOTE: workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1605
1629
  # breakage occurs when running content (stamps) are added to page
1606
1630
  update_colors if graphic_state.color_space.empty?
1607
- # NOTE: cursor advances automatically
1608
- svg_obj.draw
1631
+ 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?
1632
+ image_y = y
1633
+ image_cursor = cursor
1634
+ svg_obj.draw # NOTE: cursor advances automatically
1609
1635
  svg_obj.document.warnings.each do |img_warning|
1610
1636
  log :warn, %(problem encountered in image: #{image_path}; #{img_warning})
1611
1637
  end unless scratch?
@@ -1629,12 +1655,13 @@ module Asciidoctor
1629
1655
  end
1630
1656
  rendered_w, rendered_h = image_info.calc_image_dimensions height: available_h if rendered_h > available_h
1631
1657
  end
1632
- image_y = y
1633
- image_cursor = cursor
1634
1658
  add_dest_for_block node if node.id
1635
1659
  # NOTE: workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1636
1660
  # breakage occurs when running content (stamps) are added to page
1637
1661
  update_colors if graphic_state.color_space.empty?
1662
+ 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?
1663
+ image_y = y
1664
+ image_cursor = cursor
1638
1665
  # NOTE: specify both width and height to avoid recalculation
1639
1666
  embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
1640
1667
  draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? && (node.has_role? 'noborder')
@@ -1645,16 +1672,40 @@ module Asciidoctor
1645
1672
  move_down rendered_h if y == image_y
1646
1673
  end
1647
1674
  end
1648
- inscribe_caption node, category: :image, side: :bottom, block_align: alignment, block_width: rendered_w, max_width: @theme.image_caption_max_width if node.title?
1649
- theme_margin :block, :bottom, (next_enclosed_block node) unless pinned
1675
+ 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?
1676
+ if !pinned && (block_next = next_enclosed_block node)
1677
+ if float_to && (supports_float_wrapping? block_next) && rendered_w < bounds.width
1678
+ init_float_box node, rendered_w, rendered_h + caption_h, float_to
1679
+ else
1680
+ theme_margin :block, :bottom, block_next
1681
+ end
1682
+ end
1650
1683
  rescue => e
1651
1684
  raise if ::StopIteration === e
1652
1685
  on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{e.message}#{::Prawn::Errors::UnsupportedImageType === e && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}))
1653
1686
  end
1654
1687
  end
1655
1688
 
1689
+ def supports_float_wrapping? node
1690
+ node.context == :paragraph
1691
+ end
1692
+
1693
+ def init_float_box _node, block_width, block_height, float_to
1694
+ gap = ::Array === (gap = @theme.image_float_gap) ? gap.dup : [gap, gap]
1695
+ float_w = block_width + (gap[0] ||= 12)
1696
+ float_h = block_height + (gap[1] ||= 6)
1697
+ box_l = bounds.left + (float_to == 'right' ? 0 : float_w)
1698
+ box_t = cursor + block_height
1699
+ box_w = bounds.width - float_w
1700
+ box_r = box_l + box_w
1701
+ box_h = [box_t, float_h].min
1702
+ box_b = box_t - box_h
1703
+ move_cursor_to box_t
1704
+ @float_box = { page: page_number, top: box_t, right: box_r, bottom: box_b, left: box_l, width: box_w, height: box_h, gap: gap }
1705
+ end
1706
+
1656
1707
  def draw_image_border top, w, h, alignment
1657
- if @theme.image_border_width > 0 && @theme.image_border_color
1708
+ if (Array @theme.image_border_width).any? {|it| it&.> 0 } && (@theme.image_border_color || @theme.base_border_color)
1658
1709
  if (@theme.image_border_fit || 'content') == 'auto'
1659
1710
  bb_width = bounds.width
1660
1711
  elsif alignment == :center
@@ -1684,10 +1735,10 @@ module Asciidoctor
1684
1735
  theme_font :image_alt do
1685
1736
  alignment = (alignment = node.attr 'align') ?
1686
1737
  ((BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left) :
1687
- (resolve_alignment_from_role node.roles) || (@theme.image_align&.to_sym || :left)
1688
- inscribe_prose alt_text_template % alt_text_vars, align: alignment, margin: 0, normalize: false, single_line: true
1738
+ (resolve_text_align_from_role node.roles) || (@theme.image_align&.to_sym || :left)
1739
+ ink_prose alt_text_template % alt_text_vars, align: alignment, margin: 0, normalize: false, single_line: true
1689
1740
  end
1690
- inscribe_caption node, category: :image, side: :bottom if node.title?
1741
+ ink_caption node, category: :image, end: :bottom if node.title?
1691
1742
  theme_margin :block, :bottom, (next_enclosed_block node) unless opts[:pinned]
1692
1743
  nil
1693
1744
  end
@@ -1696,8 +1747,8 @@ module Asciidoctor
1696
1747
  add_dest_for_block node if node.id
1697
1748
  audio_path = node.media_uri node.attr 'target'
1698
1749
  play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
1699
- inscribe_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
1700
- inscribe_caption node, labeled: false, side: :bottom if node.title?
1750
+ ink_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
1751
+ ink_caption node, labeled: false, end: :bottom if node.title?
1701
1752
  theme_margin :block, :bottom, (next_enclosed_block node)
1702
1753
  end
1703
1754
 
@@ -1724,8 +1775,8 @@ module Asciidoctor
1724
1775
  if poster.nil_or_empty?
1725
1776
  add_dest_for_block node if node.id
1726
1777
  play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
1727
- inscribe_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
1728
- inscribe_caption node, labeled: false, side: :bottom if node.title?
1778
+ ink_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
1779
+ ink_caption node, labeled: false, end: :bottom if node.title?
1729
1780
  theme_margin :block, :bottom, (next_enclosed_block node)
1730
1781
  else
1731
1782
  original_attributes = node.attributes.dup
@@ -1739,7 +1790,7 @@ module Asciidoctor
1739
1790
  end
1740
1791
 
1741
1792
  # QUESTION: can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
1742
- def convert_listing_or_literal node
1793
+ def convert_code node
1743
1794
  extensions = []
1744
1795
  source_chunks = bg_color_override = font_color_override = adjusted_font_size = nil
1745
1796
  theme_font :code do
@@ -1886,17 +1937,31 @@ module Asciidoctor
1886
1937
  theme_margin :block, :bottom, (next_enclosed_block node)
1887
1938
  end
1888
1939
 
1889
- alias convert_listing convert_listing_or_literal
1890
- alias convert_literal convert_listing_or_literal
1940
+ alias convert_listing convert_code
1941
+ alias convert_literal convert_code
1942
+ alias convert_listing_or_literal convert_code
1891
1943
 
1892
1944
  def convert_pass node
1893
- node = node.dup
1894
- (subs = node.subs.dup).unshift :specialcharacters
1895
- node.instance_variable_set :@subs, subs.uniq
1896
- convert_listing_or_literal node
1945
+ theme_font :code do
1946
+ typeset_formatted_text [text: (guard_indentation node.content), color: @theme.base_font_color], (calc_line_metrics @base_line_height)
1947
+ end
1948
+ theme_margin :block, :bottom, (next_enclosed_block node)
1897
1949
  end
1898
1950
 
1899
- alias convert_stem convert_listing_or_literal
1951
+ def convert_stem node
1952
+ arrange_block node do |extent|
1953
+ add_dest_for_block node if node.id
1954
+ tare_first_page_content_stream { theme_fill_and_stroke_block :code, extent, caption_node: node }
1955
+ pad_box @theme.code_padding, node do
1956
+ theme_font :code do
1957
+ typeset_formatted_text [text: (guard_indentation node.content), color: @font_color],
1958
+ (calc_line_metrics @base_line_height),
1959
+ bottom_gutter: @bottom_gutters[-1][node]
1960
+ end
1961
+ end
1962
+ end
1963
+ theme_margin :block, :bottom, (next_enclosed_block node)
1964
+ end
1900
1965
 
1901
1966
  # Extract callout marks from string, indexed by 0-based line number
1902
1967
  # Return an Array with the processed string as the first argument
@@ -2024,6 +2089,7 @@ module Asciidoctor
2024
2089
 
2025
2090
  base_header_cell_data = nil
2026
2091
  header_cell_line_metrics = nil
2092
+ body_cell_padding = expand_padding_value theme.table_cell_padding
2027
2093
 
2028
2094
  table_data = []
2029
2095
  theme_font :table do
@@ -2038,7 +2104,7 @@ module Asciidoctor
2038
2104
  table_header_size = head_rows.size
2039
2105
  head_font_info = font_info
2040
2106
  head_line_metrics = calc_line_metrics theme.table_head_line_height || theme.table_cell_line_height || @base_line_height
2041
- head_cell_padding = expand_padding_value theme.table_head_cell_padding || theme.table_cell_padding
2107
+ head_cell_padding = ((head_cell_padding = theme.table_head_cell_padding) ? (expand_padding_value head_cell_padding) : body_cell_padding).dup
2042
2108
  head_cell_padding[0] += head_line_metrics.padding_top
2043
2109
  head_cell_padding[2] += head_line_metrics.padding_bottom
2044
2110
  # QUESTION: why doesn't text transform inherit from table?
@@ -2077,7 +2143,6 @@ module Asciidoctor
2077
2143
  text_color: @font_color,
2078
2144
  }
2079
2145
  body_cell_line_metrics = calc_line_metrics (theme.table_cell_line_height || @base_line_height)
2080
- body_cell_padding = expand_padding_value theme.table_cell_padding
2081
2146
  (body_rows + node.rows[:foot]).each do |row|
2082
2147
  table_data << (row.map do |cell|
2083
2148
  cell_data = base_cell_data.merge \
@@ -2144,7 +2209,7 @@ module Asciidoctor
2144
2209
  end
2145
2210
  # NOTE: line metrics get applied when AsciiDoc content is converted
2146
2211
  cell_line_metrics = nil
2147
- asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self, (cell_data.merge content: cell.inner_document, padding: body_cell_padding.dup)
2212
+ asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self, (cell_data.merge content: cell.inner_document, padding: body_cell_padding)
2148
2213
  cell_data = { content: asciidoc_cell }
2149
2214
  end
2150
2215
  if cell_line_metrics
@@ -2190,17 +2255,17 @@ module Asciidoctor
2190
2255
 
2191
2256
  rect_side_names = [:top, :right, :bottom, :left]
2192
2257
  grid_axis_names = [:rows, :cols]
2193
- border_color = (rect_side_names.zip expand_rect_values theme.table_border_color, (theme.base_border_color || 'transparent')).to_h
2258
+ border_color = (rect_side_names.zip expand_rect_values theme.table_border_color, 'transparent').to_h
2194
2259
  border_style = (rect_side_names.zip (expand_rect_values theme.table_border_style, :solid).map(&:to_sym)).to_h
2195
2260
  border_width = (rect_side_names.zip expand_rect_values theme.table_border_width, 0).to_h
2196
2261
  grid_color = (grid_axis_names.zip expand_grid_values (theme.table_grid_color || [border_color[:top], border_color[:left]]), 'transparent').to_h
2197
2262
  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
2198
- grid_width = (grid_axis_names.zip expand_grid_values theme.table_grid_width, 0).to_h
2263
+ grid_width = (grid_axis_names.zip expand_grid_values (theme.table_grid_width || [border_width[:top], border_width[:left]]), 0).to_h
2199
2264
 
2200
2265
  if table_header_size
2201
2266
  head_border_bottom_color = theme.table_head_border_bottom_color || grid_color[:rows]
2202
2267
  head_border_bottom_style = theme.table_head_border_bottom_style&.to_sym || grid_style[:rows]
2203
- head_border_bottom_width = theme.table_head_border_bottom_width || grid_width[:rows]
2268
+ head_border_bottom_width = theme.table_head_border_bottom_width || (grid_width[:rows] * 2.5)
2204
2269
  end
2205
2270
 
2206
2271
  case (grid = node.attr 'grid', 'all', 'table-grid')
@@ -2241,7 +2306,7 @@ module Asciidoctor
2241
2306
  alignment = theme.table_align&.to_sym || :left
2242
2307
  end
2243
2308
 
2244
- caption_side = theme.table_caption_side&.to_sym || :top
2309
+ caption_end = theme.table_caption_end&.to_sym || :top
2245
2310
  caption_max_width = theme.table_caption_max_width || 'fit-content'
2246
2311
 
2247
2312
  table_settings = {
@@ -2270,7 +2335,7 @@ module Asciidoctor
2270
2335
  table table_data, table_settings do
2271
2336
  # NOTE: call width to capture resolved table width
2272
2337
  table_width = width
2273
- @pdf.inscribe_table_caption node, alignment, table_width, caption_max_width if node.title? && caption_side == :top
2338
+ @pdf.ink_table_caption node, alignment, table_width, caption_max_width if node.title? && caption_end == :top
2274
2339
  # NOTE: align using padding instead of bounding_box as prawn-table does
2275
2340
  # using a bounding_box across pages mangles the margin box of subsequent pages
2276
2341
  if alignment != :left && table_width != (this_bounds = @pdf.bounds).width
@@ -2343,20 +2408,23 @@ module Asciidoctor
2343
2408
  bounds.subtract_left_padding left_padding
2344
2409
  bounds.subtract_right_padding right_padding if right_padding
2345
2410
  end
2346
- inscribe_table_caption node, alignment, table_width, caption_max_width, caption_side if node.title? && caption_side == :bottom
2411
+ ink_table_caption node, alignment, table_width, caption_max_width, caption_end if node.title? && caption_end == :bottom
2347
2412
  theme_margin :block, :bottom, (next_enclosed_block node)
2348
2413
  rescue ::Prawn::Errors::CannotFit
2349
2414
  log :error, (message_with_context 'cannot fit contents of table cell into specified column width', source_location: node.source_location)
2350
2415
  end
2351
2416
 
2352
2417
  def convert_thematic_break node
2353
- theme_margin :thematic_break, :top
2354
- 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)
2355
- theme_margin :thematic_break, ((block_next = next_enclosed_block node) ? :bottom : :top), block_next || true
2418
+ pad_box @theme.thematic_break_padding || [@theme.thematic_break_margin_top, 0] do
2419
+ stroke_horizontal_rule @theme.thematic_break_border_color,
2420
+ line_width: @theme.thematic_break_border_width,
2421
+ line_style: (@theme.thematic_break_border_style&.to_sym || :solid)
2422
+ end
2423
+ conceal_page_top { theme_margin :block, :bottom, (next_enclosed_block node) }
2356
2424
  end
2357
2425
 
2358
2426
  def convert_toc node, opts = {}
2359
- # NOTE: only allow document to have a single toc
2427
+ # NOTE: only allow document to have a single managed toc
2360
2428
  return if @toc_extent
2361
2429
  is_macro = (placement = opts[:placement] || 'macro') == 'macro'
2362
2430
  if ((doc = node.document).attr? 'toc-placement', placement) && (doc.attr? 'toc') && doc.sections?
@@ -2365,11 +2433,11 @@ module Asciidoctor
2365
2433
  start_new_page if @ppbook && verso_page? && !(is_macro && (node.option? 'nonfacing'))
2366
2434
  end
2367
2435
  add_dest_for_block node, id: (node.id || 'toc') if is_macro
2368
- allocate_toc doc, (doc.attr 'toclevels', 2).to_i, cursor, (title_page_on = is_book || (doc.attr? 'title-page'))
2369
- @index.start_page_number = @toc_extent.to.page + 1 if title_page_on && @theme.page_numbering_start_at == 'after-toc'
2436
+ toc_extent = @toc_extent = allocate_toc doc, (doc.attr 'toclevels', 2).to_i, cursor, (title_page_on = is_book || (doc.attr? 'title-page'))
2437
+ @index.start_page_number = toc_extent.to.page + 1 if title_page_on && @theme.page_numbering_start_at == 'after-toc'
2370
2438
  if is_macro
2371
- @disable_running_content[:header] += @toc_extent.page_range if node.option? 'noheader'
2372
- @disable_running_content[:footer] += @toc_extent.page_range if node.option? 'nofooter'
2439
+ @disable_running_content[:header] += toc_extent.page_range if node.option? 'noheader'
2440
+ @disable_running_content[:footer] += toc_extent.page_range if node.option? 'nofooter'
2373
2441
  end
2374
2442
  end
2375
2443
  nil
@@ -2387,7 +2455,7 @@ module Asciidoctor
2387
2455
 
2388
2456
  if at_page_top?
2389
2457
  if page_layout && page_layout != page.layout && page.empty?
2390
- delete_page
2458
+ delete_current_page
2391
2459
  advance_page layout: page_layout
2392
2460
  end
2393
2461
  elsif page_layout
@@ -2397,48 +2465,50 @@ module Asciidoctor
2397
2465
  end
2398
2466
  end
2399
2467
 
2400
- def convert_index_section _node
2468
+ def convert_index_section node
2401
2469
  space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
2402
- column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true do
2403
- def @bounding_box.move_past_bottom *args # rubocop:disable Lint/NestedMethodDefinition
2404
- super(*args)
2405
- @document.bounds = @parent = @document.margin_box if @current_column == 0 && @reflow_margins
2406
- end
2470
+ pagenum_sequence_style = node.document.attr 'index-pagenum-sequence-style'
2471
+ column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do
2407
2472
  @index.categories.each do |category|
2408
- # NOTE: cursor method always returns 0 inside column_box; breaks reference_bounds.move_past_bottom
2409
- bounds.move_past_bottom if space_needed_for_category > y - reference_bounds.absolute_bottom
2410
- inscribe_prose category.name,
2473
+ bounds.move_past_bottom if space_needed_for_category > cursor
2474
+ ink_prose category.name,
2411
2475
  align: :left,
2412
2476
  inline_format: false,
2413
2477
  margin_bottom: @theme.description_list_term_spacing,
2414
- style: @theme.description_list_term_font_style.to_sym
2415
- category.terms.each {|term| convert_index_list_item term }
2416
- # NOTE: see previous note for why we can't use margin_bottom method
2417
- if @theme.prose_margin_bottom > y - reference_bounds.absolute_bottom
2418
- bounds.move_past_bottom
2419
- else
2420
- move_down @theme.prose_margin_bottom
2421
- end
2478
+ style: @theme.description_list_term_font_style&.to_sym
2479
+ category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style }
2480
+ @theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom)
2422
2481
  end
2423
2482
  end
2424
2483
  nil
2425
2484
  end
2426
2485
 
2427
- def convert_index_list_item term
2486
+ def convert_index_list_item term, pagenum_sequence_style = nil
2428
2487
  text = escape_xml term.name
2429
2488
  unless term.container?
2430
2489
  if @media == 'screen'
2431
- pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2490
+ case pagenum_sequence_style
2491
+ when 'page'
2492
+ pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2493
+ when 'range'
2494
+ first_anchor_per_page = term.dests.each_with_object({}) {|dest, accum| accum[dest[:page]] ||= dest[:anchor] }
2495
+ pagenums = (consolidate_ranges first_anchor_per_page.keys).map do |range|
2496
+ anchor = first_anchor_per_page[(range.include? '-') ? (range.partition '-')[0] : range]
2497
+ %(<a anchor="#{anchor}">#{range}</a>)
2498
+ end
2499
+ else # term
2500
+ pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2501
+ end
2432
2502
  else
2433
- pagenums = consolidate_ranges term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page] } # rubocop:disable Lint/AmbiguousBlockAssociation
2503
+ pagenums = consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq
2434
2504
  end
2435
2505
  text = %(#{text}, #{pagenums.join ', '})
2436
2506
  end
2437
2507
  subterm_indent = @theme.description_list_description_indent
2438
- inscribe_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
2508
+ ink_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
2439
2509
  indent subterm_indent do
2440
2510
  term.subterms.each do |subterm|
2441
- convert_index_list_item subterm
2511
+ convert_index_list_item subterm, pagenum_sequence_style
2442
2512
  end
2443
2513
  end unless term.leaf?
2444
2514
  end
@@ -2723,7 +2793,7 @@ module Asciidoctor
2723
2793
  end
2724
2794
 
2725
2795
  # Returns a Boolean indicating whether the title page was created
2726
- def inscribe_title_page doc
2796
+ def ink_title_page doc
2727
2797
  return unless doc.header? && !doc.notitle && @theme.title_page != false
2728
2798
 
2729
2799
  # NOTE: a new page may have already been started at this point, so decide what to do with it
@@ -2799,7 +2869,7 @@ module Asciidoctor
2799
2869
  move_down @theme.title_page_title_margin_top || 0
2800
2870
  indent (@theme.title_page_title_margin_left || 0), (@theme.title_page_title_margin_right || 0) do
2801
2871
  theme_font :title_page_title do
2802
- inscribe_prose doctitle.main, align: title_align, margin: 0
2872
+ ink_prose doctitle.main, align: title_align, margin: 0
2803
2873
  end
2804
2874
  end
2805
2875
  move_down @theme.title_page_title_margin_bottom || 0
@@ -2808,7 +2878,7 @@ module Asciidoctor
2808
2878
  move_down @theme.title_page_subtitle_margin_top || 0
2809
2879
  indent (@theme.title_page_subtitle_margin_left || 0), (@theme.title_page_subtitle_margin_right || 0) do
2810
2880
  theme_font :title_page_subtitle do
2811
- inscribe_prose subtitle, align: title_align, margin: 0
2881
+ ink_prose subtitle, align: title_align, margin: 0
2812
2882
  end
2813
2883
  end
2814
2884
  move_down @theme.title_page_subtitle_margin_bottom || 0
@@ -2833,7 +2903,7 @@ module Asciidoctor
2833
2903
  end
2834
2904
  end.join @theme.title_page_authors_delimiter
2835
2905
  theme_font :title_page_authors do
2836
- inscribe_prose authors, align: title_align, margin: 0, normalize: true
2906
+ ink_prose authors, align: title_align, margin: 0, normalize: true
2837
2907
  end
2838
2908
  end
2839
2909
  move_down @theme.title_page_authors_margin_bottom || 0
@@ -2846,18 +2916,18 @@ module Asciidoctor
2846
2916
  end
2847
2917
  indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
2848
2918
  theme_font :title_page_revision do
2849
- inscribe_prose revision_text, align: title_align, margin: 0, normalize: false
2919
+ ink_prose revision_text, align: title_align, margin: 0, normalize: false
2850
2920
  end
2851
2921
  end
2852
2922
  move_down @theme.title_page_revision_margin_bottom || 0
2853
2923
  end
2854
2924
  end
2855
2925
 
2856
- inscribe_prose DummyText, margin: 0, line_height: 1, normalize: false if page.empty?
2926
+ ink_prose DummyText, margin: 0, line_height: 1, normalize: false if page.empty?
2857
2927
  true
2858
2928
  end
2859
2929
 
2860
- def inscribe_cover_page doc, face
2930
+ def ink_cover_page doc, face
2861
2931
  image_path, image_opts = resolve_background_image doc, @theme, %(#{face}-cover-image), theme_key: %(cover_#{face}_image).to_sym, symbolic_paths: ['', '~']
2862
2932
  if image_path
2863
2933
  if image_path.empty?
@@ -2903,92 +2973,178 @@ module Asciidoctor
2903
2973
 
2904
2974
  def start_new_chapter chapter
2905
2975
  start_new_page unless at_page_top?
2906
- # TODO: must call update_colors before advancing to next page if start_new_page is called in inscribe_chapter_title
2976
+ # TODO: must call update_colors before advancing to next page if start_new_page is called in ink_chapter_title
2907
2977
  start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
2908
2978
  end
2909
2979
 
2910
2980
  alias start_new_part start_new_chapter
2911
2981
 
2912
- def arrange_section sect, title, opts
2913
- if sect.option? 'breakable'
2982
+ # Position the cursor for where to ink the specified section title or discrete heading node.
2983
+ #
2984
+ # This method computes whether there is enough room on the page to prevent the specified node
2985
+ # from being orphaned. If there is not enough room, the method will advance the cursor to
2986
+ # the next page. This method is not called if the cursor is already at the top of the page or
2987
+ # whether this node has no node that follows it in document order.
2988
+ def arrange_heading node, title, opts
2989
+ if node.option? 'breakable'
2914
2990
  orphaned = nil
2915
2991
  dry_run single_page: true do
2916
2992
  start_page = page
2917
2993
  theme_font :heading, level: opts[:level] do
2918
2994
  if opts[:part]
2919
- inscribe_part_title sect, title, opts
2995
+ ink_part_title node, title, opts
2920
2996
  elsif opts[:chapterlike]
2921
- inscribe_chapter_title sect, title, opts
2997
+ ink_chapter_title node, title, opts
2922
2998
  else
2923
- inscribe_general_heading sect, title, opts
2999
+ ink_general_heading node, title, opts
2924
3000
  end
2925
3001
  end
2926
3002
  if page == start_page
2927
3003
  page.tare_content_stream
2928
- orphaned = stop_if_first_page_empty { traverse sect }
3004
+ orphaned = stop_if_first_page_empty do
3005
+ node.context == :section ? (traverse node) : (convert (siblings = node.parent.blocks)[(siblings.index node) + 1])
3006
+ end
2929
3007
  end
2930
3008
  end
2931
3009
  start_new_page if orphaned
2932
3010
  else
2933
3011
  theme_font :heading, level: (hlevel = opts[:level]) do
2934
- # FIXME: this height doesn't account for impact of text transform or inline formatting
2935
- heading_height =
2936
- (height_of_typeset_text title) +
2937
- (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
2938
- (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom)
2939
- heading_height += @theme.heading_min_height_after if sect.blocks? && @theme.heading_min_height_after
2940
- start_new_page unless cursor > heading_height
3012
+ h_padding_t, h_padding_r, h_padding_b, h_padding_l = expand_padding_value @theme[%(heading_h#{hlevel}_padding)]
3013
+ h_fits = indent h_padding_l, h_padding_r do
3014
+ # FIXME: this height doesn't account for impact of text transform or inline formatting
3015
+ heading_h = (height_of_typeset_text title) +
3016
+ (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
3017
+ (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom) + h_padding_t + h_padding_b
3018
+ if (min_height_after = @theme.heading_min_height_after) &&
3019
+ (node.context == :section ? node.blocks? : node != node.parent.blocks[-1])
3020
+ heading_h += min_height_after
3021
+ end
3022
+ cursor >= heading_h
3023
+ end
3024
+ start_new_page unless h_fits
2941
3025
  end
2942
3026
  end
2943
3027
  nil
2944
3028
  end
2945
3029
 
2946
- def inscribe_chapter_title node, title, opts = {}
2947
- inscribe_general_heading node, title, (opts.merge outdent: true)
3030
+ def ink_chapter_title node, title, opts = {}
3031
+ ink_general_heading node, title, (opts.merge outdent: true)
2948
3032
  end
2949
3033
 
2950
- alias inscribe_part_title inscribe_chapter_title
3034
+ alias ink_part_title ink_chapter_title
2951
3035
 
2952
- def inscribe_general_heading _node, title, opts = {}
2953
- inscribe_heading title, opts
3036
+ def ink_general_heading _node, title, opts = {}
3037
+ ink_heading title, opts
2954
3038
  end
2955
3039
 
2956
- # NOTE: inscribe_heading doesn't set the theme font because it's used for various types of headings
2957
- def inscribe_heading string, opts = {}
2958
- hlevel = opts[:level]
3040
+ # NOTE: ink_heading doesn't set the theme font because it's used for various types of headings
3041
+ def ink_heading string, opts = {}
3042
+ if (h_level = opts[:level])
3043
+ h_category = %(heading_h#{h_level})
3044
+ end
2959
3045
  unless (top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top))
2960
3046
  if at_page_top?
2961
- if hlevel && (top_margin = @theme[%(heading_h#{hlevel}_margin_page_top)] || @theme.heading_margin_page_top) > 0
3047
+ if h_category && (top_margin = @theme[%(#{h_category}_margin_page_top)] || @theme.heading_margin_page_top) > 0
2962
3048
  move_down top_margin
2963
3049
  end
2964
3050
  top_margin = 0
2965
3051
  else
2966
- top_margin = (hlevel ? @theme[%(heading_h#{hlevel}_margin_top)] : nil) || @theme.heading_margin_top
3052
+ top_margin = (h_category ? @theme[%(#{h_category}_margin_top)] : nil) || @theme.heading_margin_top
2967
3053
  end
2968
3054
  end
2969
- bot_margin = margin || (opts.delete :margin_bottom) || (hlevel ? @theme[%(heading_h#{hlevel}_margin_bottom)] : nil) || @theme.heading_margin_bottom
3055
+ bot_margin = margin || (opts.delete :margin_bottom) || (h_category ? @theme[%(#{h_category}_margin_bottom)] : nil) || @theme.heading_margin_bottom
2970
3056
  if (transform = resolve_text_transform opts)
2971
3057
  string = transform_text string, transform
2972
3058
  end
2973
3059
  outdent_section opts.delete :outdent do
2974
3060
  margin_top top_margin
2975
- # QUESTION: should we move inherited styles to typeset_text?
2976
- if (inherited = apply_text_decoration font_styles, :heading, hlevel).empty?
2977
- inline_format_opts = true
2978
- else
2979
- inline_format_opts = [{ inherited: inherited }]
3061
+ start_cursor = cursor
3062
+ start_page_number = page_number
3063
+ pad_box h_category ? @theme[%(#{h_category}_padding)] : nil do
3064
+ # QUESTION: should we move inherited styles to typeset_text?
3065
+ if (inherited = apply_text_decoration font_styles, :heading, h_level).empty?
3066
+ inline_format_opts = true
3067
+ else
3068
+ inline_format_opts = [{ inherited: inherited }]
3069
+ end
3070
+ typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
3071
+ color: @font_color,
3072
+ inline_format: inline_format_opts,
3073
+ align: @base_text_align.to_sym,
3074
+ }.merge(opts)
3075
+ end
3076
+ if h_category && @theme[%(#{h_category}_border_width)] &&
3077
+ (@theme[%(#{h_category}_border_color)] || @theme.base_border_color) && page_number == start_page_number
3078
+ float do
3079
+ bounding_box [bounds.left, start_cursor], width: bounds.width, height: start_cursor - cursor do
3080
+ theme_fill_and_stroke_bounds h_category
3081
+ end
3082
+ end
2980
3083
  end
2981
- typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
2982
- color: @font_color,
2983
- inline_format: inline_format_opts,
2984
- align: @base_text_align.to_sym,
2985
- }.merge(opts)
2986
3084
  margin_bottom bot_margin
2987
3085
  end
2988
3086
  end
2989
3087
 
3088
+ # private
3089
+ def ink_paragraph_in_float_box node, float_box, prose_opts, role_keys, block_next, insert_margin_bottom
3090
+ @float_box = para_font_descender = para_font_size = end_cursor = nil
3091
+ if role_keys
3092
+ line_metrics = theme_font_cascade role_keys do
3093
+ para_font_descender = font.descender
3094
+ para_font_size = font_size
3095
+ calc_line_metrics @base_line_height
3096
+ end
3097
+ else
3098
+ para_font_descender = font.descender
3099
+ para_font_size = font_size
3100
+ line_metrics = calc_line_metrics @base_line_height
3101
+ end
3102
+ # allocate the space of at least one empty line below block
3103
+ line_height_length = line_metrics.height + line_metrics.leading + line_metrics.padding_top
3104
+ start_page_number = float_box[:page]
3105
+ start_cursor = cursor
3106
+ block_bottom = (float_box_bottom = float_box[:bottom]) + float_box[:gap][1]
3107
+ # use :at to incorporate padding top from line metrics
3108
+ # use :final_gap to incorporate padding bottom from line metrics
3109
+ # use :draw_text_callback to track end cursor (requires applying :final_gap to result manually)
3110
+ prose_opts.update \
3111
+ at: [float_box[:left], start_cursor - line_metrics.padding_top],
3112
+ width: float_box[:width],
3113
+ height: [cursor, float_box[:height] - (float_box[:top] - start_cursor) + line_height_length].min,
3114
+ final_gap: para_font_descender + line_metrics.padding_bottom,
3115
+ draw_text_callback: (proc do |text, opts|
3116
+ draw_text! text, opts
3117
+ end_cursor = opts[:at][1] # does not include :final_gap value
3118
+ end)
3119
+ overflow_text = role_keys ?
3120
+ theme_font_cascade(role_keys) { ink_prose node.content, prose_opts } :
3121
+ (ink_prose node.content, prose_opts)
3122
+ move_cursor_to end_cursor -= prose_opts[:final_gap] if end_cursor # ink_prose with :height does not move cursor
3123
+ if overflow_text.empty?
3124
+ if block_next && (supports_float_wrapping? block_next)
3125
+ insert_margin_bottom.call
3126
+ @float_box = float_box if page_number == start_page_number && cursor > start_cursor - prose_opts[:height]
3127
+ elsif end_cursor > block_bottom
3128
+ move_cursor_to block_bottom
3129
+ theme_margin :block, :bottom, block_next
3130
+ else
3131
+ insert_margin_bottom.call
3132
+ end
3133
+ else
3134
+ overflow_prose_opts = { align: prose_opts[:align] || @base_text_align.to_sym }
3135
+ unless end_cursor
3136
+ overflow_prose_opts[:indent_paragraphs] = prose_opts[:indent_paragraphs]
3137
+ move_cursor_to float_box_bottom if start_cursor > float_box_bottom
3138
+ end
3139
+ role_keys ?
3140
+ theme_font_cascade(role_keys) { typeset_formatted_text overflow_text, line_metrics, overflow_prose_opts } :
3141
+ (typeset_formatted_text overflow_text, line_metrics, overflow_prose_opts)
3142
+ insert_margin_bottom.call
3143
+ end
3144
+ end
3145
+
2990
3146
  # NOTE: inline_format is true by default
2991
- def inscribe_prose string, opts = {}
3147
+ def ink_prose string, opts = {}
2992
3148
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || 0
2993
3149
  bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
2994
3150
  if (transform = resolve_text_transform opts)
@@ -3009,12 +3165,13 @@ module Asciidoctor
3009
3165
  text_decoration_width: (opts.delete :text_decoration_width),
3010
3166
  }.compact
3011
3167
  end
3012
- typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
3168
+ result = typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
3013
3169
  color: @font_color,
3014
3170
  inline_format: [inline_format_opts],
3015
3171
  align: @base_text_align.to_sym,
3016
3172
  }.merge(opts)
3017
3173
  margin_bottom bot_margin
3174
+ result
3018
3175
  end
3019
3176
 
3020
3177
  def generate_manname_section node
@@ -3035,13 +3192,14 @@ module Asciidoctor
3035
3192
  # The subject argument can either be a String or an AbstractNode. If
3036
3193
  # subject is an AbstractNode, only call this method if the node has a
3037
3194
  # title (i.e., subject.title? returns true).
3038
- def inscribe_caption subject, opts = {}
3195
+ def ink_caption subject, opts = {}
3039
3196
  if opts.delete :dry_run
3040
- force_top_margin = !at_page_top? if (force_top_margin = opts.delete :force_top_margin).nil?
3041
3197
  return (dry_run keep_together: true, single_page: :enforce do
3042
- # TODO: encapsulate this logic to force top margin to be applied
3043
- margin_box.instance_variable_set :@y, margin_box.absolute_top + 0.0001 if force_top_margin
3044
- inscribe_caption subject, opts
3198
+ if opts.delete :force_top_margin
3199
+ conceal_page_top { ink_caption subject, opts }
3200
+ else
3201
+ ink_caption subject, opts
3202
+ end
3045
3203
  end).single_page_height
3046
3204
  end
3047
3205
  if ::Asciidoctor::AbstractBlock === subject
@@ -3069,11 +3227,22 @@ module Asciidoctor
3069
3227
  if (max_width = opts.delete :max_width) && max_width != 'none'
3070
3228
  if ::String === max_width
3071
3229
  if max_width.start_with? 'fit-content'
3072
- if max_width.end_with? 't', '()'
3073
- max_width = block_width || container_width
3074
- else
3075
- max_width = (block_width || container_width) * (max_width.slice 12, max_width.length - 1).to_f / 100.0
3230
+ block_width ||= container_width
3231
+ unless max_width.end_with? 't', '()'
3232
+ max_width = block_width * (max_width.slice 12, max_width.length - 1).to_f / 100.0
3233
+ if (caption_width_delta = block_width - max_width) > 0
3234
+ case align
3235
+ when :right
3236
+ indent_by[0] += caption_width_delta
3237
+ when :center
3238
+ indent_by[0] += caption_width_delta * 0.5
3239
+ indent_by[1] += caption_width_delta * 0.5
3240
+ else # :left, nil
3241
+ indent_by[1] += caption_width_delta
3242
+ end
3243
+ end
3076
3244
  end
3245
+ max_width = block_width
3077
3246
  elsif max_width.end_with? '%'
3078
3247
  max_width = [max_width.to_f / 100 * bounds.width, bounds.width].min
3079
3248
  block_align = align
@@ -3088,16 +3257,17 @@ module Asciidoctor
3088
3257
  if (remainder = container_width - max_width) > 0
3089
3258
  case block_align
3090
3259
  when :right
3091
- indent_by = [remainder, 0]
3260
+ indent_by[0] += remainder
3092
3261
  when :center
3093
- indent_by = [(side_margin = remainder * 0.5), side_margin]
3262
+ indent_by[0] += remainder * 0.5
3263
+ indent_by[1] += remainder * 0.5
3094
3264
  else # :left, nil
3095
- indent_by = [0, remainder]
3265
+ indent_by[1] += remainder
3096
3266
  end
3097
3267
  end
3098
3268
  end
3099
3269
  theme_font_cascade [:caption, category_caption] do
3100
- if ((opts.delete :side) || :top) == :top
3270
+ if ((opts.delete :end) || (opts.delete :side) || :top) == :top
3101
3271
  margin = { top: caption_margin_outside, bottom: caption_margin_inside }
3102
3272
  else
3103
3273
  margin = { top: caption_margin_inside, bottom: caption_margin_outside }
@@ -3111,7 +3281,7 @@ module Asciidoctor
3111
3281
  float { bounding_box(fill_at, width: container_width, height: caption_height) { fill_bounds bg_color } }
3112
3282
  end
3113
3283
  indent(*indent_by) do
3114
- inscribe_prose string, ({
3284
+ ink_prose string, ({
3115
3285
  margin_top: margin[:top],
3116
3286
  margin_bottom: margin[:bottom],
3117
3287
  align: text_align,
@@ -3125,16 +3295,22 @@ module Asciidoctor
3125
3295
  end
3126
3296
 
3127
3297
  # Render the caption for a table and return the height of the rendered content
3128
- def inscribe_table_caption node, table_alignment = :left, table_width = nil, max_width = nil, side = :top
3129
- inscribe_caption node, category: :table, side: side, block_align: table_alignment, block_width: table_width, max_width: max_width
3298
+ def ink_table_caption node, table_alignment = :left, table_width = nil, max_width = nil, end_ = :top
3299
+ ink_caption node, category: :table, end: end_, block_align: table_alignment, block_width: table_width, max_width: max_width
3130
3300
  end
3131
3301
 
3132
3302
  def allocate_toc doc, toc_num_levels, toc_start_cursor, title_page_on
3133
- toc_start_page = page_number
3303
+ toc_start_page_number = page_number
3304
+ to_page = nil
3134
3305
  extent = dry_run onto: self do
3135
- inscribe_toc doc, toc_num_levels, toc_start_page, toc_start_cursor
3306
+ to_page = (ink_toc doc, toc_num_levels, toc_start_page_number, toc_start_cursor).end
3136
3307
  margin_bottom @theme.block_margin_bottom unless title_page_on
3137
3308
  end
3309
+ # NOTE: patch for custom converters that allocate extra TOC pages without actually creating them
3310
+ if to_page > extent.to.page
3311
+ extent.to.page = to_page
3312
+ extent.to.cursor = bounds.height
3313
+ end
3138
3314
  # NOTE: reserve pages for the toc; leaves cursor on page after last page in toc
3139
3315
  if title_page_on
3140
3316
  extent.each_page { start_new_page }
@@ -3142,18 +3318,22 @@ module Asciidoctor
3142
3318
  extent.each_page {|first_page| start_new_page unless first_page }
3143
3319
  move_cursor_to extent.to.cursor
3144
3320
  end
3145
- @toc_extent = extent
3321
+ extent
3322
+ end
3323
+
3324
+ def get_entries_for_toc node
3325
+ node.sections
3146
3326
  end
3147
3327
 
3148
3328
  # NOTE: num_front_matter_pages not used during a dry run
3149
- def inscribe_toc doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0
3329
+ def ink_toc doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0
3150
3330
  go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
3151
3331
  start_page_number = page_number
3152
3332
  move_cursor_to start_cursor
3153
3333
  unless (toc_title = doc.attr 'toc-title').nil_or_empty?
3154
3334
  theme_font_cascade [[:heading, level: 2], :toc_title] do
3155
3335
  toc_title_align = (@theme.toc_title_text_align || @theme.heading_h2_text_align || @theme.heading_text_align || @base_text_align).to_sym
3156
- inscribe_general_heading doc, toc_title, align: toc_title_align, level: 2, outdent: true, role: :toctitle
3336
+ ink_general_heading doc, toc_title, align: toc_title_align, level: 2, outdent: true, role: :toctitle
3157
3337
  end
3158
3338
  end
3159
3339
  # QUESTION: should we skip this whole method if num_levels < 0?
@@ -3178,7 +3358,7 @@ module Asciidoctor
3178
3358
  }
3179
3359
  end
3180
3360
  theme_margin :toc, :top
3181
- inscribe_toc_level doc.sections, num_levels, dot_leader, num_front_matter_pages
3361
+ ink_toc_level (get_entries_for_toc doc), num_levels, dot_leader, num_front_matter_pages
3182
3362
  end
3183
3363
  # NOTE: range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
3184
3364
  toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
@@ -3186,38 +3366,48 @@ module Asciidoctor
3186
3366
  toc_page_numbers
3187
3367
  end
3188
3368
 
3189
- def inscribe_toc_level sections, num_levels, dot_leader, num_front_matter_pages
3369
+ def ink_toc_level entries, num_levels, dot_leader, num_front_matter_pages
3190
3370
  # NOTE: font options aren't always reliable, so store size separately
3191
3371
  toc_font_info = theme_font :toc do
3192
3372
  { font: font, size: @font_size }
3193
3373
  end
3194
3374
  hanging_indent = @theme.toc_hanging_indent
3195
- sections.each do |sect|
3196
- next if (num_levels_for_sect = (sect.attr 'toclevels', num_levels).to_i) < sect.level
3197
- theme_font :toc, level: (sect.level + 1) do
3198
- sect_title = @text_transform ? (transform_text sect.numbered_title, @text_transform) : sect.numbered_title
3199
- next if sect_title.empty?
3375
+ entries.each do |entry|
3376
+ next if (num_levels_for_entry = (entry.attr 'toclevels', num_levels).to_i) < (entry_level = entry.level + 1).pred
3377
+ theme_font :toc, level: entry_level do
3378
+ next unless (entry_anchor = (entry.attr 'pdf-anchor') || entry.id)
3379
+ entry_title = entry.context == :section ? entry.numbered_title : (entry.title? ? entry.title : (entry.xreftext 'basic'))
3380
+ next if entry_title.empty?
3381
+ entry_title = transform_text entry_title, @text_transform if @text_transform
3200
3382
  pgnum_label_placeholder_width = rendered_width_of_string '0' * @toc_max_pagenum_digits
3201
- # NOTE: only write section title (excluding dots and page number) if this is a dry run
3383
+ # NOTE: only write title (excluding dots and page number) if this is a dry run
3202
3384
  if scratch?
3203
3385
  indent 0, pgnum_label_placeholder_width do
3204
3386
  # NOTE: must wrap title in empty anchor element in case links are styled with different font family / size
3205
- inscribe_prose sect_title, anchor: true, normalize: false, hanging_indent: hanging_indent, normalize_line_height: true, margin: 0
3387
+ ink_prose entry_title, anchor: true, normalize: false, hanging_indent: hanging_indent, normalize_line_height: true, margin: 0
3206
3388
  end
3207
3389
  else
3208
- physical_pgnum = sect.attr 'pdf-page-start'
3209
- virtual_pgnum = physical_pgnum - num_front_matter_pages
3210
- pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
3390
+ if !(physical_pgnum = entry.attr 'pdf-page-start') &&
3391
+ (target_page_ref = (get_dest entry_anchor)&.first) &&
3392
+ (target_page_idx = state.pages.index {|candidate| candidate.dictionary == target_page_ref })
3393
+ physical_pgnum = target_page_idx + 1
3394
+ end
3395
+ if physical_pgnum
3396
+ virtual_pgnum = physical_pgnum - num_front_matter_pages
3397
+ pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
3398
+ else
3399
+ pgnum_label = '?'
3400
+ end
3211
3401
  start_page_number = page_number
3212
3402
  start_cursor = cursor
3213
3403
  start_dots = nil
3214
- sect_title_inherited = (apply_text_decoration ::Set.new, :toc, sect.level.next).merge anchor: (sect_anchor = sect.attr 'pdf-anchor'), color: @font_color
3404
+ entry_title_inherited = (apply_text_decoration ::Set.new, :toc, entry_level).merge anchor: entry_anchor, color: @font_color
3215
3405
  # NOTE: use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag
3216
- sect_title_fragments = text_formatter.format sect_title, inherited: sect_title_inherited
3406
+ entry_title_fragments = text_formatter.format entry_title, inherited: entry_title_inherited
3217
3407
  line_metrics = calc_line_metrics @base_line_height
3218
3408
  indent 0, pgnum_label_placeholder_width do
3219
- (sect_title_fragments[-1][:callback] ||= []) << (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
3220
- typeset_formatted_text sect_title_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true
3409
+ (entry_title_fragments[-1][:callback] ||= []) << (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
3410
+ typeset_formatted_text entry_title_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true
3221
3411
  start_dots = last_fragment_pos.right + hanging_indent
3222
3412
  last_fragment_cursor = last_fragment_pos.top + line_metrics.padding_top
3223
3413
  start_cursor = last_fragment_cursor if last_fragment_pos.page_number > start_page_number || (start_cursor - last_fragment_cursor) > line_metrics.height
@@ -3225,7 +3415,7 @@ module Asciidoctor
3225
3415
  end_cursor = cursor
3226
3416
  move_cursor_to start_cursor
3227
3417
  # NOTE: we're guaranteed to be on the same page as the final line of the entry
3228
- if dot_leader[:width] > 0 && (dot_leader[:levels].include? sect.level)
3418
+ if dot_leader[:width] > 0 && (dot_leader[:levels].include? entry_level.pred)
3229
3419
  pgnum_label_width = rendered_width_of_string pgnum_label
3230
3420
  pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
3231
3421
  save_font do
@@ -3237,18 +3427,18 @@ module Asciidoctor
3237
3427
  typeset_formatted_text [
3238
3428
  { text: dot_leader[:text] * num_dots, color: dot_leader[:font_color] },
3239
3429
  dot_leader[:spacer],
3240
- ({ text: pgnum_label, anchor: sect_anchor }.merge pgnum_label_font_settings),
3430
+ ({ text: pgnum_label, anchor: entry_anchor }.merge pgnum_label_font_settings),
3241
3431
  ], line_metrics, align: :right
3242
3432
  end
3243
3433
  else
3244
- typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: sect_anchor }], line_metrics, align: :right
3434
+ typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: entry_anchor }], line_metrics, align: :right
3245
3435
  end
3246
3436
  move_cursor_to end_cursor
3247
3437
  end
3248
3438
  end
3249
3439
  indent @theme.toc_indent do
3250
- inscribe_toc_level sect.sections, num_levels_for_sect, dot_leader, num_front_matter_pages
3251
- end if num_levels_for_sect > sect.level
3440
+ ink_toc_level (get_entries_for_toc entry), num_levels_for_entry, dot_leader, num_front_matter_pages
3441
+ end if num_levels_for_entry >= entry_level
3252
3442
  end
3253
3443
  end
3254
3444
 
@@ -3275,15 +3465,15 @@ module Asciidoctor
3275
3465
  icon_data
3276
3466
  end
3277
3467
 
3278
- # TODO: delegate to inscribe_page_header and inscribe_page_footer per page
3279
- def inscribe_running_content periphery, doc, skip = [1, 1], body_start_page_number = 1
3468
+ # TODO: delegate to ink_page_header and ink_page_footer per page
3469
+ def ink_running_content periphery, doc, skip = [1, 1], body_start_page_number = 1
3280
3470
  skip_pages, skip_pagenums = skip
3281
3471
  # NOTE: find and advance to first non-imported content page to use as model page
3282
- return unless (content_start_page = state.pages[skip_pages..-1].index {|it| !it.imported_page? })
3283
- content_start_page += (skip_pages + 1)
3472
+ return unless (content_start_page_number = state.pages[skip_pages..-1].index {|it| !it.imported_page? })
3473
+ content_start_page_number += (skip_pages + 1)
3284
3474
  num_pages = page_count
3285
3475
  prev_page_number = page_number
3286
- go_to_page content_start_page
3476
+ go_to_page content_start_page_number
3287
3477
 
3288
3478
  # FIXME: probably need to treat doctypes differently
3289
3479
  is_book = doc.doctype == 'book'
@@ -3379,7 +3569,7 @@ module Asciidoctor
3379
3569
  pagenums_enabled = doc.attr? 'pagenums'
3380
3570
  periphery_layout_cache = {}
3381
3571
  # NOTE: this block is invoked during PDF generation, after #write -> #render_file and thus after #convert_document
3382
- repeat (content_start_page..num_pages), dynamic: true do
3572
+ repeat (content_start_page_number..num_pages), dynamic: true do
3383
3573
  pgnum = page_number
3384
3574
  # NOTE: don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
3385
3575
  next if page.imported_page? || (disable_on_pages.include? pgnum)
@@ -3416,7 +3606,7 @@ module Asciidoctor
3416
3606
  theme_font periphery do
3417
3607
  canvas do
3418
3608
  bounding_box [trim_styles[:content_left][side], trim_styles[:top][side]], width: trim_styles[:content_width][side], height: trim_styles[:height] do
3419
- if (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
3609
+ if trim_styles[:column_rule_color] && (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
3420
3610
  trim_column_rule_spacing = trim_styles[:column_rule_spacing]
3421
3611
  else
3422
3612
  trim_column_rule_width = nil
@@ -3468,8 +3658,9 @@ module Asciidoctor
3468
3658
  content = apply_subs_discretely doc, content, drop_lines_with_unresolved_attributes: true, imagesdir: @themesdir
3469
3659
  content = transform_text content, @text_transform if @text_transform
3470
3660
  end
3471
- formatted_text_box (parse_text content, color: @font_color, inline_format: [normalize: true]),
3661
+ formatted_text_box (parse_text content, inline_format: [normalize: true]),
3472
3662
  at: [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset] + ((Array trim_styles[:valign])[0] == :center ? font.descender * 0.5 : 0)],
3663
+ color: @font_color,
3473
3664
  width: colwidth,
3474
3665
  height: trim_styles[:prose_content_height][side],
3475
3666
  align: colspec[:align],
@@ -3501,7 +3692,7 @@ module Asciidoctor
3501
3692
  trim_content_margin_recto = @theme[%(#{periphery}_recto_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
3502
3693
  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 }
3503
3694
  if (trim_padding_recto = @theme[%(#{periphery}_recto_padding)] || @theme[%(#{periphery}_padding)])
3504
- trim_padding_recto = (expand_margin_value trim_padding_recto).map.with_index {|v, i| v + trim_content_margin_recto[i] }
3695
+ trim_padding_recto = (expand_padding_value trim_padding_recto).map.with_index {|v, i| v + trim_content_margin_recto[i] }
3505
3696
  else
3506
3697
  trim_padding_recto = trim_content_margin_recto
3507
3698
  end
@@ -3511,7 +3702,7 @@ module Asciidoctor
3511
3702
  trim_content_margin_verso = @theme[%(#{periphery}_verso_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
3512
3703
  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 }
3513
3704
  if (trim_padding_verso = @theme[%(#{periphery}_verso_padding)] || @theme[%(#{periphery}_padding)])
3514
- trim_padding_verso = (expand_margin_value trim_padding_verso).map.with_index {|v, i| v + trim_content_margin_verso[i] }
3705
+ trim_padding_verso = (expand_padding_value trim_padding_verso).map.with_index {|v, i| v + trim_content_margin_verso[i] }
3515
3706
  else
3516
3707
  trim_padding_verso = trim_content_margin_verso
3517
3708
  end
@@ -3524,12 +3715,12 @@ module Asciidoctor
3524
3715
  # NOTE: we've already verified this property is set
3525
3716
  height: (trim_height = @theme[%(#{periphery}_height)]),
3526
3717
  bg_color: (resolve_theme_color %(#{periphery}_background_color).to_sym),
3527
- border_color: (trim_border_color = resolve_theme_color %(#{periphery}_border_color).to_sym),
3718
+ border_width: (trim_border_width = @theme[%(#{periphery}_border_width)] || 0),
3719
+ border_color: trim_border_width > 0 ? (resolve_theme_color %(#{periphery}_border_color).to_sym, @theme.base_border_color) : nil,
3528
3720
  border_style: (@theme[%(#{periphery}_border_style)]&.to_sym || :solid),
3529
- border_width: (trim_border_width = trim_border_color ? @theme[%(#{periphery}_border_width)] || @theme.base_border_width : 0),
3530
- column_rule_color: (trim_column_rule_color = resolve_theme_color %(#{periphery}_column_rule_color).to_sym),
3721
+ column_rule_width: (trim_column_rule_width = @theme[%(#{periphery}_column_rule_width)] || 0),
3722
+ column_rule_color: trim_column_rule_width > 0 ? (resolve_theme_color %(#{periphery}_column_rule_color).to_sym) : nil,
3531
3723
  column_rule_style: (@theme[%(#{periphery}_column_rule_style)]&.to_sym || :solid),
3532
- column_rule_width: (trim_column_rule_color ? @theme[%(#{periphery}_column_rule_width)] || 0 : 0),
3533
3724
  column_rule_spacing: (@theme[%(#{periphery}_column_rule_spacing)] || 0),
3534
3725
  valign: valign_offset ? [valign, valign_offset] : valign,
3535
3726
  img_valign: @theme[%(#{periphery}_image_vertical_align)],
@@ -3697,8 +3888,9 @@ module Asciidoctor
3697
3888
  outline.define do
3698
3889
  initial_pagenum = has_front_cover ? 2 : 1
3699
3890
  # FIXME: use sanitize: :plain_text on Document#doctitle once available
3700
- if document.page_count >= initial_pagenum && (doctitle = document.resolve_doctitle doc)
3701
- page title: (document.sanitize doctitle), destination: (document.dest_top initial_pagenum)
3891
+ if document.page_count >= initial_pagenum && (outline_title = doc.attr 'outline-title') &&
3892
+ (outline_title.empty? ? (outline_title = document.resolve_doctitle doc) : outline_title)
3893
+ page title: (document.sanitize outline_title), destination: (document.dest_top initial_pagenum)
3702
3894
  end
3703
3895
  # QUESTION: is there any way to get add_outline_level to invoke in the context of the outline?
3704
3896
  document.add_outline_level self, doc.sections, num_levels, expand_levels
@@ -3760,7 +3952,14 @@ module Asciidoctor
3760
3952
  pdf_doc.render_file target
3761
3953
  # QUESTION: restore attributes first?
3762
3954
  @pdfmark&.generate_file target
3763
- (Optimizer.new @optimize, pdf_doc.min_version).optimize_file target if @optimize
3955
+ if (quality = @optimize)
3956
+ if quality.include? ','
3957
+ quality, compliance = quality.split ',', 2
3958
+ elsif quality.include? '/'
3959
+ quality, compliance = nil, quality
3960
+ end
3961
+ (Optimizer.new quality, pdf_doc.min_version, compliance).optimize_file target
3962
+ end
3764
3963
  to_file = true
3765
3964
  end
3766
3965
  if !ENV['KEEP_ARTIFACTS']
@@ -3812,7 +4011,7 @@ module Asciidoctor
3812
4011
  end
3813
4012
 
3814
4013
  def resolve_text_transform key, use_fallback = true
3815
- if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key.to_s])
4014
+ if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key])
3816
4015
  transform == 'none' ? nil : transform
3817
4016
  elsif use_fallback
3818
4017
  @text_transform
@@ -3821,9 +4020,9 @@ module Asciidoctor
3821
4020
 
3822
4021
  # QUESTION: should we pass a category as an argument?
3823
4022
  # QUESTION: should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
3824
- def resolve_theme_color key, fallback_color = nil
3825
- if (color = @theme[key.to_s]) && color != 'transparent'
3826
- color
4023
+ def resolve_theme_color key, fallback_color = nil, transparent_color = fallback_color
4024
+ if (color = @theme[key])
4025
+ color == 'transparent' ? transparent_color : color
3827
4026
  else
3828
4027
  fallback_color
3829
4028
  end
@@ -3834,7 +4033,7 @@ module Asciidoctor
3834
4033
  end
3835
4034
 
3836
4035
  def theme_fill_and_stroke_bounds category, opts = {}
3837
- fill_and_stroke_bounds opts[:background_color], @theme[%(#{category}_border_color)],
4036
+ fill_and_stroke_bounds opts[:background_color], @theme[%(#{category}_border_color)] || @theme.base_border_color,
3838
4037
  line_width: @theme[%(#{category}_border_width)],
3839
4038
  line_style: (@theme[%(#{category}_border_style)]&.to_sym || :solid),
3840
4039
  radius: @theme[%(#{category}_border_radius)]
@@ -3843,7 +4042,7 @@ module Asciidoctor
3843
4042
  def theme_fill_and_stroke_block category, extent, opts = {}
3844
4043
  node_with_caption = nil unless (node_with_caption = opts[:caption_node])&.title?
3845
4044
  unless extent
3846
- inscribe_caption node_with_caption, category: category if node_with_caption
4045
+ ink_caption node_with_caption, category: category if node_with_caption
3847
4046
  return
3848
4047
  end
3849
4048
  if (b_width = (opts.key? :border_width) ? opts[:border_width] : @theme[%(#{category}_border_width)])
@@ -3857,14 +4056,12 @@ module Asciidoctor
3857
4056
  bg_color = nil
3858
4057
  end
3859
4058
  unless b_width || bg_color
3860
- inscribe_caption node_with_caption, category: category if node_with_caption
4059
+ ink_caption node_with_caption, category: category if node_with_caption
3861
4060
  return
3862
4061
  end
3863
- if (b_color = @theme[%(#{category}_border_color)]) == 'transparent'
3864
- b_color = @page_bg_color
3865
- end
4062
+ b_color = resolve_theme_color %(#{category}_border_color).to_sym, @theme.base_border_color, @page_bg_color
3866
4063
  b_radius ||= (@theme[%(#{category}_border_radius)] || 0) + (b_width || 0)
3867
- if b_width && b_color
4064
+ if b_width
3868
4065
  if b_color == @page_bg_color # let page background cut into block background
3869
4066
  b_gap_color, b_shift = @page_bg_color, (b_width * 0.5)
3870
4067
  elsif (b_gap_color = bg_color) && b_gap_color != b_color
@@ -3875,7 +4072,7 @@ module Asciidoctor
3875
4072
  else # let page background cut into block background; guarantees b_width is set
3876
4073
  b_shift, b_gap_color = (b_width ||= 0.5) * 0.5, @page_bg_color
3877
4074
  end
3878
- inscribe_caption node_with_caption, category: category if node_with_caption
4075
+ ink_caption node_with_caption, category: category if node_with_caption
3879
4076
  extent.from.page += 1 unless extent.from.page == page_number # sanity check
3880
4077
  float do
3881
4078
  extent.each_page do |first_page, last_page|
@@ -4012,9 +4209,7 @@ module Asciidoctor
4012
4209
  # NOTE: it also removes zero-width spaces
4013
4210
  arranger.finalize_line
4014
4211
  actual_width = width_of_fragments arranger.fragments
4015
- unless ::Array === (padding = @theme[%(#{category}_padding)])
4016
- padding = ::Array.new 4, padding
4017
- end
4212
+ padding = expand_padding_value @theme[%(#{category}_padding)]
4018
4213
  if actual_width > (available_width = bounds.width - padding[3].to_f - padding[1].to_f)
4019
4214
  adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate 4
4020
4215
  if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
@@ -4088,9 +4283,10 @@ module Asciidoctor
4088
4283
 
4089
4284
  # TODO: document me, esp the first line formatting functionality
4090
4285
  def typeset_text string, line_metrics, opts = {}
4091
- move_down line_metrics.padding_top
4092
4286
  opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
4093
4287
  string = string.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
4288
+ return text_box string, opts if opts[:height]
4289
+ move_down line_metrics.padding_top
4094
4290
  if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
4095
4291
  indent hanging_indent do
4096
4292
  text string, (opts.merge indent_paragraphs: -hanging_indent)
@@ -4254,10 +4450,11 @@ module Asciidoctor
4254
4450
  end
4255
4451
  end
4256
4452
 
4257
- def resolve_alignment_from_role roles, use_theme: false
4453
+ def resolve_text_align_from_role roles, query_theme: false, remove_predefined: false
4258
4454
  if (align_role = roles.reverse.find {|r| TextAlignmentRoles.include? r })
4455
+ roles.replace roles - TextAlignmentRoles if remove_predefined
4259
4456
  (align_role.slice 5, align_role.length).to_sym
4260
- elsif use_theme
4457
+ elsif query_theme
4261
4458
  roles.reverse.each do |role|
4262
4459
  if (align = @theme[%(role_#{role}_text_align)])
4263
4460
  return align.to_sym
@@ -4267,6 +4464,9 @@ module Asciidoctor
4267
4464
  end
4268
4465
  end
4269
4466
 
4467
+ # Deprecated
4468
+ alias resolve_alignment_from_role resolve_text_align_from_role
4469
+
4270
4470
  # QUESTION: is this method still necessary?
4271
4471
  def resolve_imagesdir doc
4272
4472
  if (imagesdir = doc.attr 'imagesdir').nil_or_empty? || (imagesdir = imagesdir.chomp '/') == '.'
@@ -4596,24 +4796,35 @@ module Asciidoctor
4596
4796
  end
4597
4797
 
4598
4798
  # Deprecated method names
4599
- alias layout_footnotes inscribe_footnotes
4600
- alias layout_title_page inscribe_title_page
4601
- alias layout_cover_page inscribe_cover_page
4602
- alias layout_chapter_title inscribe_chapter_title
4603
- alias layout_part_title inscribe_part_title
4604
- alias layout_general_heading inscribe_general_heading
4605
- alias layout_heading inscribe_heading
4606
- alias layout_prose inscribe_prose
4607
- alias layout_caption inscribe_caption
4608
- alias layout_table_caption inscribe_table_caption
4609
- alias layout_toc inscribe_toc
4610
- alias layout_toc_level inscribe_toc_level
4611
- alias layout_running_content inscribe_running_content
4612
-
4613
- def self.method_added method
4614
- if (method_name = method.to_s).start_with? 'layout_'
4615
- alias_method %(inscribe_#{method_name.slice 7, method_name.length}).to_sym, method
4616
- end
4799
+ alias layout_footnotes ink_footnotes
4800
+ alias layout_title_page ink_title_page
4801
+ alias layout_cover_page ink_cover_page
4802
+ alias layout_chapter_title ink_chapter_title
4803
+ alias layout_part_title ink_part_title
4804
+ alias layout_general_heading ink_general_heading
4805
+ alias layout_heading ink_heading
4806
+ alias layout_prose ink_prose
4807
+ alias layout_caption ink_caption
4808
+ alias layout_table_caption ink_table_caption
4809
+ alias layout_toc ink_toc
4810
+ alias layout_toc_level ink_toc_level
4811
+ alias layout_running_content ink_running_content
4812
+
4813
+ # intercepts "class CustomPDFConverter < (Asciidoctor::Converter.for 'pdf')"
4814
+ def self.method_added method_sym
4815
+ if (method_name = method_sym.to_s).start_with? 'layout_'
4816
+ alias_method %(ink_#{method_name.slice 7, method_name.length}).to_sym, method_sym
4817
+ elsif method_name == 'convert_listing_or_literal' || method_name == 'convert_code'
4818
+ alias_method :convert_listing, method_sym
4819
+ alias_method :convert_literal, method_sym
4820
+ end
4821
+ end
4822
+
4823
+ # intercepts "(Asciidoctor::Converter.for 'pdf').prepend CustomConverterExtensions"
4824
+ def self.prepend *mods
4825
+ super
4826
+ mods.each {|mod| (mod.instance_methods false).each {|method| method_added method } }
4827
+ self
4617
4828
  end
4618
4829
 
4619
4830
  private