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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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]
@@ -486,8 +487,9 @@ module Asciidoctor
486
487
  theme.code_linenum_font_color ||= '999999'
487
488
  theme.callout_list_margin_top_after_code ||= 0
488
489
  theme.role_unresolved_font_color ||= 'FF0000'
489
- theme.index_columns ||= 2
490
490
  theme.footnotes_item_spacing ||= 0
491
+ theme.index_columns ||= 2
492
+ theme.index_column_gap ||= theme.base_font_size
491
493
  theme.kbd_separator ||= '+'
492
494
  theme.title_page_authors_delimiter ||= ', '
493
495
  theme.title_page_revision_delimiter ||= ', '
@@ -665,24 +667,24 @@ module Asciidoctor
665
667
  title, _, subtitle = title.rpartition sep
666
668
  title = %(#{title}\n<em class="subtitle">#{subtitle}</em>)
667
669
  end
668
- hlevel = sect.level + 1
670
+ hlevel = sect.level.next
669
671
  align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
670
672
  chapterlike = !(part = sectname == 'part') && (sectname == 'chapter' || (sect.document.doctype == 'book' && sect.level == 1))
671
673
  hidden = sect.option? 'notitle'
672
674
  hopts = { align: align, level: hlevel, part: part, chapterlike: chapterlike, outdent: !(part || chapterlike) }
673
675
  if part
674
676
  unless @theme.heading_part_break_before == 'auto'
675
- start_new = true
677
+ started_new = true
676
678
  start_new_part sect
677
679
  end
678
680
  elsif chapterlike
679
681
  if @theme.heading_chapter_break_before != 'auto' ||
680
682
  (@theme.heading_part_break_after == 'always' && sect == sect.parent.sections[0])
681
- start_new = true
683
+ started_new = true
682
684
  start_new_chapter sect
683
685
  end
684
686
  end
685
- 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?
686
688
  # QUESTION: should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
687
689
  sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
688
690
  # QUESTION: should we just assign the section this generated id?
@@ -753,14 +755,17 @@ module Asciidoctor
753
755
  end
754
756
 
755
757
  def convert_floating_title node
756
- add_dest_for_block node if node.id
758
+ title = node.title
757
759
  hlevel = node.level.next
758
- unless (align = resolve_alignment_from_role node.roles)
760
+ unless (align = resolve_text_align_from_role node.roles)
759
761
  align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
760
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
761
766
  # QUESTION: should we decouple styles from section titles?
762
767
  theme_font :heading, level: hlevel do
763
- ink_general_heading node, node.title, align: align, level: hlevel, outdent: (node.parent.context == :section)
768
+ ink_general_heading node, title, hopts
764
769
  end
765
770
  end
766
771
 
@@ -778,11 +783,21 @@ module Asciidoctor
778
783
  end
779
784
  # FIXME: allow theme to control more first line options
780
785
  if (line1_font_style = @theme.abstract_first_line_font_style&.to_sym) && line1_font_style != font_style
781
- 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
782
794
  end
783
795
  if (line1_font_color = @theme.abstract_first_line_font_color)
784
796
  (first_line_options ||= {})[:color] = line1_font_color
785
797
  end
798
+ if (line1_text_transform = @theme.abstract_first_line_text_transform)
799
+ (first_line_options ||= {})[:text_transform] = line1_text_transform
800
+ end
786
801
  prose_opts[:first_line_options] = first_line_options if first_line_options
787
802
  # FIXME: make this cleaner!!
788
803
  if node.blocks?
@@ -791,7 +806,7 @@ module Asciidoctor
791
806
  if child.context == :paragraph
792
807
  child.document.playback_attributes child.attributes
793
808
  prose_opts[:margin_bottom] = 0 if child == last_block
794
- ink_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)
795
810
  prose_opts.delete :first_line_options
796
811
  prose_opts.delete :margin_bottom
797
812
  else
@@ -800,7 +815,7 @@ module Asciidoctor
800
815
  end
801
816
  end
802
817
  elsif node.content_model != :compound && (string = node.content)
803
- if (align = resolve_alignment_from_role node.roles)
818
+ if (align = resolve_text_align_from_role node.roles)
804
819
  prose_opts[:align] = align
805
820
  end
806
821
  ink_prose string, (prose_opts.merge margin_bottom: 0)
@@ -824,39 +839,39 @@ module Asciidoctor
824
839
 
825
840
  def convert_paragraph node
826
841
  add_dest_for_block node if node.id
842
+
827
843
  prose_opts = { margin_bottom: 0, hyphenate: true }
828
- 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)
829
845
  prose_opts[:align] = align
830
- roles -= TextAlignmentRoles
831
846
  end
832
-
847
+ role_keys = roles.map {|role| %(role_#{role}).to_sym } unless roles.empty?
833
848
  if (text_indent = @theme.prose_text_indent) > 0 ||
834
849
  ((text_indent = @theme.prose_text_indent_inner) > 0 &&
835
850
  (self_idx = (siblings = node.parent.blocks).index node) > 0 && siblings[self_idx - 1].context == :paragraph)
836
851
  prose_opts[:indent_paragraphs] = text_indent
837
852
  end
838
-
839
- # TODO: check if we're within one line of the bottom of the page
840
- # and advance to the next page if so (similar to logic for section titles)
841
- ink_caption node, labeled: false if node.title?
842
-
843
853
  if (bottom_gutter = @bottom_gutters[-1][node])
844
854
  prose_opts[:bottom_gutter] = bottom_gutter
845
855
  end
846
856
 
847
- if roles.empty?
848
- ink_prose node.content, prose_opts
849
- else
850
- theme_font_cascade (roles.map {|role| %(role_#{role}).to_sym }) do
851
- ink_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
852
864
  end
853
865
  end
854
866
 
855
- block_next = next_enclosed_block node
856
- if (margin_inner_val = @theme.prose_margin_inner) && block_next&.context == :paragraph
857
- 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
858
869
  else
859
- 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
860
875
  end
861
876
  end
862
877
 
@@ -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
- 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)
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
@@ -1360,7 +1379,7 @@ module Asciidoctor
1360
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
@@ -1553,9 +1572,13 @@ 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
@@ -1563,6 +1586,7 @@ module Asciidoctor
1563
1586
 
1564
1587
  caption_end = @theme.image_caption_end&.to_sym || :bottom
1565
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')
1566
1590
  # NOTE: if width is not set explicitly and max-width is fit-content, caption height may not be accurate
1567
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
1568
1592
 
@@ -1570,7 +1594,7 @@ module Asciidoctor
1570
1594
  pinned = opts[:pinned]
1571
1595
 
1572
1596
  begin
1573
- rendered_w = nil
1597
+ rendered_h = rendered_w = nil
1574
1598
  span_page_width_if align_to_page do
1575
1599
  if image_format == 'svg'
1576
1600
  if ::Base64 === image_path
@@ -1649,13 +1673,37 @@ module Asciidoctor
1649
1673
  end
1650
1674
  end
1651
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?
1652
- theme_margin :block, :bottom, (next_enclosed_block node) unless pinned
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
1653
1683
  rescue => e
1654
1684
  raise if ::StopIteration === e
1655
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' : ''}))
1656
1686
  end
1657
1687
  end
1658
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
+
1659
1707
  def draw_image_border top, w, h, alignment
1660
1708
  if (Array @theme.image_border_width).any? {|it| it&.> 0 } && (@theme.image_border_color || @theme.base_border_color)
1661
1709
  if (@theme.image_border_fit || 'content') == 'auto'
@@ -1687,7 +1735,7 @@ module Asciidoctor
1687
1735
  theme_font :image_alt do
1688
1736
  alignment = (alignment = node.attr 'align') ?
1689
1737
  ((BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left) :
1690
- (resolve_alignment_from_role node.roles) || (@theme.image_align&.to_sym || :left)
1738
+ (resolve_text_align_from_role node.roles) || (@theme.image_align&.to_sym || :left)
1691
1739
  ink_prose alt_text_template % alt_text_vars, align: alignment, margin: 0, normalize: false, single_line: true
1692
1740
  end
1693
1741
  ink_caption node, category: :image, end: :bottom if node.title?
@@ -1742,7 +1790,7 @@ module Asciidoctor
1742
1790
  end
1743
1791
 
1744
1792
  # QUESTION: can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
1745
- def convert_listing_or_literal node
1793
+ def convert_code node
1746
1794
  extensions = []
1747
1795
  source_chunks = bg_color_override = font_color_override = adjusted_font_size = nil
1748
1796
  theme_font :code do
@@ -1889,17 +1937,31 @@ module Asciidoctor
1889
1937
  theme_margin :block, :bottom, (next_enclosed_block node)
1890
1938
  end
1891
1939
 
1892
- alias convert_listing convert_listing_or_literal
1893
- 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
1894
1943
 
1895
1944
  def convert_pass node
1896
- node = node.dup
1897
- (subs = node.subs.dup).unshift :specialcharacters
1898
- node.instance_variable_set :@subs, subs.uniq
1899
- 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)
1900
1949
  end
1901
1950
 
1902
- 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
1903
1965
 
1904
1966
  # Extract callout marks from string, indexed by 0-based line number
1905
1967
  # Return an Array with the processed string as the first argument
@@ -2353,11 +2415,12 @@ module Asciidoctor
2353
2415
  end
2354
2416
 
2355
2417
  def convert_thematic_break node
2356
- theme_margin :thematic_break, :top
2357
- stroke_horizontal_rule @theme.thematic_break_border_color,
2358
- line_width: @theme.thematic_break_border_width,
2359
- line_style: (@theme.thematic_break_border_style&.to_sym || :solid)
2360
- 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) }
2361
2424
  end
2362
2425
 
2363
2426
  def convert_toc node, opts = {}
@@ -2405,26 +2468,16 @@ module Asciidoctor
2405
2468
  def convert_index_section node
2406
2469
  space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
2407
2470
  pagenum_sequence_style = node.document.attr 'index-pagenum-sequence-style'
2408
- column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true do
2409
- def @bounding_box.move_past_bottom *args # rubocop:disable Lint/NestedMethodDefinition
2410
- super(*args)
2411
- @document.bounds = @parent = @document.margin_box if @current_column == 0 && @reflow_margins
2412
- end
2471
+ column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do
2413
2472
  @index.categories.each do |category|
2414
- # NOTE: cursor method always returns 0 inside column_box; breaks reference_bounds.move_past_bottom
2415
- bounds.move_past_bottom if space_needed_for_category > y - reference_bounds.absolute_bottom
2473
+ bounds.move_past_bottom if space_needed_for_category > cursor
2416
2474
  ink_prose category.name,
2417
2475
  align: :left,
2418
2476
  inline_format: false,
2419
2477
  margin_bottom: @theme.description_list_term_spacing,
2420
2478
  style: @theme.description_list_term_font_style&.to_sym
2421
2479
  category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style }
2422
- # NOTE: see previous note for why we can't use margin_bottom method
2423
- if @theme.prose_margin_bottom > y - reference_bounds.absolute_bottom
2424
- bounds.move_past_bottom
2425
- else
2426
- move_down @theme.prose_margin_bottom
2427
- end
2480
+ @theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom)
2428
2481
  end
2429
2482
  end
2430
2483
  nil
@@ -2926,23 +2979,31 @@ module Asciidoctor
2926
2979
 
2927
2980
  alias start_new_part start_new_chapter
2928
2981
 
2929
- def arrange_section sect, title, opts
2930
- 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'
2931
2990
  orphaned = nil
2932
2991
  dry_run single_page: true do
2933
2992
  start_page = page
2934
2993
  theme_font :heading, level: opts[:level] do
2935
2994
  if opts[:part]
2936
- ink_part_title sect, title, opts
2995
+ ink_part_title node, title, opts
2937
2996
  elsif opts[:chapterlike]
2938
- ink_chapter_title sect, title, opts
2997
+ ink_chapter_title node, title, opts
2939
2998
  else
2940
- ink_general_heading sect, title, opts
2999
+ ink_general_heading node, title, opts
2941
3000
  end
2942
3001
  end
2943
3002
  if page == start_page
2944
3003
  page.tare_content_stream
2945
- 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
2946
3007
  end
2947
3008
  end
2948
3009
  start_new_page if orphaned
@@ -2954,7 +3015,10 @@ module Asciidoctor
2954
3015
  heading_h = (height_of_typeset_text title) +
2955
3016
  (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
2956
3017
  (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom) + h_padding_t + h_padding_b
2957
- heading_h += @theme.heading_min_height_after if @theme.heading_min_height_after && sect.blocks?
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
2958
3022
  cursor >= heading_h
2959
3023
  end
2960
3024
  start_new_page unless h_fits
@@ -3021,6 +3085,64 @@ module Asciidoctor
3021
3085
  end
3022
3086
  end
3023
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
+
3024
3146
  # NOTE: inline_format is true by default
3025
3147
  def ink_prose string, opts = {}
3026
3148
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || 0
@@ -3043,12 +3165,13 @@ module Asciidoctor
3043
3165
  text_decoration_width: (opts.delete :text_decoration_width),
3044
3166
  }.compact
3045
3167
  end
3046
- 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), {
3047
3169
  color: @font_color,
3048
3170
  inline_format: [inline_format_opts],
3049
3171
  align: @base_text_align.to_sym,
3050
3172
  }.merge(opts)
3051
3173
  margin_bottom bot_margin
3174
+ result
3052
3175
  end
3053
3176
 
3054
3177
  def generate_manname_section node
@@ -3071,11 +3194,12 @@ module Asciidoctor
3071
3194
  # title (i.e., subject.title? returns true).
3072
3195
  def ink_caption subject, opts = {}
3073
3196
  if opts.delete :dry_run
3074
- force_top_margin = !at_page_top? if (force_top_margin = opts.delete :force_top_margin).nil?
3075
3197
  return (dry_run keep_together: true, single_page: :enforce do
3076
- # TODO: encapsulate this logic to force top margin to be applied
3077
- margin_box.instance_variable_set :@y, margin_box.absolute_top + 0.0001 if force_top_margin
3078
- ink_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
3079
3203
  end).single_page_height
3080
3204
  end
3081
3205
  if ::Asciidoctor::AbstractBlock === subject
@@ -3103,11 +3227,22 @@ module Asciidoctor
3103
3227
  if (max_width = opts.delete :max_width) && max_width != 'none'
3104
3228
  if ::String === max_width
3105
3229
  if max_width.start_with? 'fit-content'
3106
- if max_width.end_with? 't', '()'
3107
- max_width = block_width || container_width
3108
- else
3109
- 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
3110
3244
  end
3245
+ max_width = block_width
3111
3246
  elsif max_width.end_with? '%'
3112
3247
  max_width = [max_width.to_f / 100 * bounds.width, bounds.width].min
3113
3248
  block_align = align
@@ -3122,11 +3257,12 @@ module Asciidoctor
3122
3257
  if (remainder = container_width - max_width) > 0
3123
3258
  case block_align
3124
3259
  when :right
3125
- indent_by = [remainder, 0]
3260
+ indent_by[0] += remainder
3126
3261
  when :center
3127
- indent_by = [(side_margin = remainder * 0.5), side_margin]
3262
+ indent_by[0] += remainder * 0.5
3263
+ indent_by[1] += remainder * 0.5
3128
3264
  else # :left, nil
3129
- indent_by = [0, remainder]
3265
+ indent_by[1] += remainder
3130
3266
  end
3131
3267
  end
3132
3268
  end
@@ -3522,8 +3658,9 @@ module Asciidoctor
3522
3658
  content = apply_subs_discretely doc, content, drop_lines_with_unresolved_attributes: true, imagesdir: @themesdir
3523
3659
  content = transform_text content, @text_transform if @text_transform
3524
3660
  end
3525
- formatted_text_box (parse_text content, color: @font_color, inline_format: [normalize: true]),
3661
+ formatted_text_box (parse_text content, inline_format: [normalize: true]),
3526
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,
3527
3664
  width: colwidth,
3528
3665
  height: trim_styles[:prose_content_height][side],
3529
3666
  align: colspec[:align],
@@ -4146,9 +4283,10 @@ module Asciidoctor
4146
4283
 
4147
4284
  # TODO: document me, esp the first line formatting functionality
4148
4285
  def typeset_text string, line_metrics, opts = {}
4149
- move_down line_metrics.padding_top
4150
4286
  opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
4151
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
4152
4290
  if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
4153
4291
  indent hanging_indent do
4154
4292
  text string, (opts.merge indent_paragraphs: -hanging_indent)
@@ -4312,10 +4450,11 @@ module Asciidoctor
4312
4450
  end
4313
4451
  end
4314
4452
 
4315
- def resolve_alignment_from_role roles, use_theme: false
4453
+ def resolve_text_align_from_role roles, query_theme: false, remove_predefined: false
4316
4454
  if (align_role = roles.reverse.find {|r| TextAlignmentRoles.include? r })
4455
+ roles.replace roles - TextAlignmentRoles if remove_predefined
4317
4456
  (align_role.slice 5, align_role.length).to_sym
4318
- elsif use_theme
4457
+ elsif query_theme
4319
4458
  roles.reverse.each do |role|
4320
4459
  if (align = @theme[%(role_#{role}_text_align)])
4321
4460
  return align.to_sym
@@ -4325,6 +4464,9 @@ module Asciidoctor
4325
4464
  end
4326
4465
  end
4327
4466
 
4467
+ # Deprecated
4468
+ alias resolve_alignment_from_role resolve_text_align_from_role
4469
+
4328
4470
  # QUESTION: is this method still necessary?
4329
4471
  def resolve_imagesdir doc
4330
4472
  if (imagesdir = doc.attr 'imagesdir').nil_or_empty? || (imagesdir = imagesdir.chomp '/') == '.'
@@ -4669,9 +4811,12 @@ module Asciidoctor
4669
4811
  alias layout_running_content ink_running_content
4670
4812
 
4671
4813
  # intercepts "class CustomPDFConverter < (Asciidoctor::Converter.for 'pdf')"
4672
- def self.method_added method
4673
- if (method_name = method.to_s).start_with? 'layout_'
4674
- alias_method %(ink_#{method_name.slice 7, method_name.length}).to_sym, method
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
4675
4820
  end
4676
4821
  end
4677
4822
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Prawn::Document::ColumnBox.prepend (Module.new do
4
+ def absolute_bottom
5
+ stretchy? ? @parent.absolute_bottom : super
6
+ end
7
+
8
+ def move_past_bottom *_args
9
+ initial_page = @document.page
10
+ super
11
+ if (page = @document.page) != initial_page && page.margins != initial_page.margins
12
+ @document.bounds = self.class.new @document, @parent, (margin_box = @document.margin_box).absolute_top_left,
13
+ columns: @columns, reflow_margins: true, spacer: @spacer, width: margin_box.width
14
+ end
15
+ end
16
+ end)