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

Sign up to get free protection for your applications and to get access to all the features.
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