asciidoctor-pdf 2.1.5 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,8 +46,9 @@ module Asciidoctor
46
46
  tip: { name: 'far-lightbulb', stroke_color: '111111', size: 24 },
47
47
  warning: { name: 'fas-exclamation-triangle', stroke_color: 'BF6900', size: 24 },
48
48
  }
49
- TextAlignmentNames = %w(justify left center right)
50
- TextAlignmentRoles = %w(text-justify text-left text-center text-right)
49
+ TextAlignmentNames = { 'justify' => true, 'left' => true, 'center' => true, 'right' => true }
50
+ IndentableTextAlignments = { justify: true, left: true }
51
+ TextAlignmentRoles = { 'text-justify' => true, 'text-left' => true, 'text-center' => true, 'text-right' => true }
51
52
  TextDecorationStyleTable = { 'underline' => :underline, 'line-through' => :strikethrough }
52
53
  FontKerningTable = { 'normal' => true, 'none' => false }
53
54
  BlockFloatNames = %w(left right)
@@ -102,6 +103,7 @@ module Asciidoctor
102
103
  'filled' => (?\u2776..?\u277f).to_a + (?\u24eb..?\u24f4).to_a,
103
104
  }
104
105
  TypographicQuotes = %w(“ ” ‘ ’)
106
+ InlineFormatSniffRx = /[<&]/
105
107
  SimpleAttributeRefRx = /(?<!\\)\{\w+(?:-\w+)*\}/
106
108
  MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|p[txc])?'
107
109
  MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|p[txc])?$/
@@ -160,11 +162,11 @@ module Asciidoctor
160
162
  doc.promote_preface_block
161
163
  init_pdf doc
162
164
  # set default value for outline, outline-title, and pagenums attributes if not otherwise set
163
- doc.attributes['outline'] = '' unless (doc.attribute_locked? 'outline') || ((doc.instance_variable_get :@attributes_modified).include? 'outline')
164
- doc.attributes['outline-title'] = '' unless (doc.attribute_locked? 'outline-title') || ((doc.instance_variable_get :@attributes_modified).include? 'outline-title')
165
- doc.attributes['pagenums'] = '' unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
165
+ doc.attributes['outline'] = '' if doc.attr_unspecified? 'outline'
166
+ doc.attributes['outline-title'] = '' if doc.attr_unspecified? 'outline-title'
167
+ doc.attributes['pagenums'] = '' if doc.attr_unspecified? 'pagenums'
166
168
 
167
- on_page_create(&(method :init_page))
169
+ on_page_create(&(method :init_page).curry[doc])
168
170
 
169
171
  marked_page_number = page_number
170
172
  # NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF
@@ -179,7 +181,7 @@ module Asciidoctor
179
181
  end
180
182
  start_new_page
181
183
  else
182
- @page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress' && page_number == 0
184
+ @page_margin[:cover] = @page_margin[page.layout][:recto] if @media == 'prepress' && page_number == 0
183
185
  start_new_page unless page&.empty? # rubocop:disable Lint/SafeNavigationWithEmpty
184
186
  # NOTE: the base font must be set before any content is written to the main or scratch document
185
187
  # this method is called inside ink_title_page if the title page is active
@@ -200,19 +202,24 @@ module Asciidoctor
200
202
  if (insert_toc = (doc.attr? 'toc') && !((toc_placement = doc.attr 'toc-placement') == 'macro' || toc_placement == 'preamble') && !(get_entries_for_toc doc).empty?)
201
203
  start_new_page if @ppbook && verso_page?
202
204
  add_dest_for_block doc, id: 'toc', y: (at_page_top? ? page_height : nil)
203
- @toc_extent = allocate_toc doc, toc_num_levels, cursor, title_page_on
205
+ @toc_extent = allocate_toc doc, toc_num_levels, cursor, (title_page_on && theme.toc_break_after != 'auto')
204
206
  else
205
207
  @toc_extent = nil
206
208
  end
207
209
 
208
- start_new_page if @ppbook && verso_page? && !(((next_block = doc.first_child)&.context == :preamble ? next_block.first_child : next_block)&.option? 'nonfacing')
210
+ if @ppbook && verso_page? && !(((next_block = doc.first_child)&.context == :preamble ? next_block.first_child : next_block)&.option? 'nonfacing')
211
+ min_start_at = 0
212
+ start_new_page
213
+ else
214
+ min_start_at = 1
215
+ end
209
216
 
210
217
  if title_page_on
211
218
  zero_page_offset = has_front_cover ? 1 : 0
212
219
  first_page_offset = has_title_page ? zero_page_offset.next : zero_page_offset
213
220
  body_offset = (body_start_page_number = page_number) - 1
214
221
  if ::Integer === (running_content_start_at = @theme.running_content_start_at)
215
- running_content_body_offset = body_offset + [running_content_start_at.pred, 0].max
222
+ running_content_body_offset = body_offset + [running_content_start_at.pred, min_start_at.pred].max
216
223
  running_content_start_at = 'body'
217
224
  else
218
225
  running_content_body_offset = body_offset
@@ -226,7 +233,7 @@ module Asciidoctor
226
233
  end
227
234
  end
228
235
  if ::Integer === (page_numbering_start_at = @theme.page_numbering_start_at)
229
- page_numbering_body_offset = body_offset + [page_numbering_start_at.pred, 0].max
236
+ page_numbering_body_offset = body_offset + [page_numbering_start_at.pred, min_start_at.pred].max
230
237
  page_numbering_start_at = 'body'
231
238
  else
232
239
  page_numbering_body_offset = body_offset
@@ -351,7 +358,6 @@ module Asciidoctor
351
358
  @cache_uri = doc.attr? 'cache-uri'
352
359
  @jail_dir = doc.safe < ::Asciidoctor::SafeMode::SAFE ? nil : doc.base_dir
353
360
  @media ||= doc.attr 'media', 'screen'
354
- @page_margin_by_side = { recto: (page_margin_recto = page_margin), verso: (page_margin_verso = page_margin), cover: page_margin }
355
361
  case doc.attr 'pdf-folio-placement', (@media == 'prepress' ? 'physical' : 'virtual')
356
362
  when 'physical'
357
363
  @folio_placement = { basis: :physical }
@@ -362,6 +368,13 @@ module Asciidoctor
362
368
  else
363
369
  @folio_placement = { basis: :virtual }
364
370
  end
371
+ @page_margin = { cover: page_margin }
372
+ @page_margin[:portrait] = @page_margin[:landscape] = { recto: (page_margin_recto = page_margin), verso: (page_margin_verso = page_margin) }
373
+ if (rotated_page_margin = resolve_page_margin (doc.attr 'pdf-page-margin-rotated') || theme.page_margin_rotated)
374
+ rotated_page_margin = expand_margin_value rotated_page_margin
375
+ @edge_shorthand_cache = nil
376
+ @page_margin[PageLayouts[(PageLayouts.index page.layout) - 1]] = { recto: rotated_page_margin, verso: rotated_page_margin.dup }
377
+ end
365
378
  if @media == 'prepress'
366
379
  @ppbook = doc.doctype == 'book'
367
380
  if (page_margin_outer = theme.page_margin_outer)
@@ -373,17 +386,7 @@ module Asciidoctor
373
386
  else
374
387
  @ppbook = nil
375
388
  end
376
- if (bg_image = resolve_background_image doc, theme, 'page-background-image')&.first
377
- @page_bg_image = { verso: bg_image, recto: bg_image }
378
- else
379
- @page_bg_image = { verso: nil, recto: nil }
380
- end
381
- if (bg_image = resolve_background_image doc, theme, 'page-background-image-verso')
382
- @page_bg_image[:verso] = bg_image[0] && bg_image
383
- end
384
- if (bg_image = resolve_background_image doc, theme, 'page-background-image-recto')
385
- @page_bg_image[:recto] = bg_image[0] && bg_image
386
- end
389
+ @page_bg_image = {}
387
390
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
388
391
  # QUESTION: should ThemeLoader handle registering fonts instead?
389
392
  register_fonts theme.font_catalog, ((doc.attr 'pdf-fontsdir')&.sub '{docdir}', (doc.attr 'docdir')) || 'GEM_FONTS_DIR'
@@ -393,11 +396,10 @@ module Asciidoctor
393
396
  @font_scale = 1
394
397
  @font_color = theme.base_font_color
395
398
  @text_decoration_width = theme.base_text_decoration_width
396
- @base_text_align = (text_align = doc.attr 'text-align') && (TextAlignmentNames.include? text_align) ? text_align : theme.base_text_align
399
+ @base_text_align = (text_align = doc.attr 'text-align') && TextAlignmentNames[text_align] ? text_align : theme.base_text_align
397
400
  @base_line_height = theme.base_line_height
398
401
  @cjk_line_breaks = doc.attr? 'scripts', 'cjk'
399
- if (hyphen_lang = (doc.attr 'hyphens') ||
400
- (((doc.attribute_locked? 'hyphens') || ((doc.instance_variable_get :@attributes_modified).include? 'hyphens')) ? nil : @theme.base_hyphens)) &&
402
+ if (hyphen_lang = (doc.attr 'hyphens') || ((doc.attr_unspecified? 'hyphens') ? @theme.base_hyphens : nil)) &&
401
403
  ((defined? ::Text::Hyphen::VERSION) || !(Helpers.require_library 'text/hyphen', 'text-hyphen', :warn).nil?)
402
404
  hyphen_lang = doc.attr 'lang' if !(::String === hyphen_lang) || hyphen_lang.empty?
403
405
  hyphen_lang = 'en_us' if hyphen_lang.nil_or_empty? || hyphen_lang == 'en'
@@ -435,34 +437,7 @@ module Asciidoctor
435
437
  end
436
438
 
437
439
  def build_pdf_options doc, theme
438
- case (page_margin = (doc.attr 'pdf-page-margin') || theme.page_margin)
439
- when ::Array
440
- if page_margin.empty?
441
- page_margin = nil
442
- else
443
- page_margin = page_margin.slice 0, 4 if page_margin.length > 4
444
- page_margin = page_margin.map {|v| ::Numeric === v ? v : (str_to_pt v.to_s) }
445
- end
446
- when ::Numeric
447
- page_margin = [page_margin]
448
- when ::String
449
- if page_margin.empty?
450
- page_margin = nil
451
- elsif (page_margin.start_with? '[') && (page_margin.end_with? ']')
452
- if (page_margin = (page_margin.slice 1, page_margin.length - 2).rstrip).empty?
453
- page_margin = nil
454
- else
455
- if (page_margin = page_margin.split ',', -1).length > 4
456
- page_margin = page_margin.slice 0, 4
457
- end
458
- page_margin = page_margin.map {|v| str_to_pt v.rstrip }
459
- end
460
- else
461
- page_margin = [(str_to_pt page_margin)]
462
- end
463
- else
464
- page_margin = nil
465
- end
440
+ page_margin = resolve_page_margin (doc.attr 'pdf-page-margin') || theme.page_margin
466
441
 
467
442
  if (doc.attr? 'pdf-page-size') && PageSizeRx =~ (doc.attr 'pdf-page-size')
468
443
  # e.g, [8.5in, 11in]
@@ -560,8 +535,10 @@ module Asciidoctor
560
535
  elsif (theme_name = doc.attr 'pdf-theme')
561
536
  theme = ThemeLoader.load_theme theme_name, (user_themesdir = (doc.attr 'pdf-themesdir')&.sub '{docdir}', (doc.attr 'docdir'))
562
537
  @themesdir = theme.__dir__
563
- else
538
+ elsif (doc.attr 'media', 'screen') == 'screen'
564
539
  @themesdir = (theme = ThemeLoader.load_theme).__dir__
540
+ else
541
+ @themesdir = (theme = ThemeLoader.load_theme 'default-for-print').__dir__
565
542
  end
566
543
  prepare_theme theme
567
544
  rescue
@@ -604,6 +581,7 @@ module Asciidoctor
604
581
  theme.code_linenum_font_color ||= '999999'
605
582
  theme.callout_list_margin_top_after_code ||= 0
606
583
  theme.role_unresolved_font_color ||= 'FF0000'
584
+ theme.footnotes_margin_top ||= 'auto'
607
585
  theme.footnotes_item_spacing ||= 0
608
586
  theme.index_columns ||= 2
609
587
  theme.index_column_gap ||= theme.base_font_size
@@ -768,22 +746,16 @@ module Asciidoctor
768
746
  pagenums = consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq
769
747
  end
770
748
  pagenums.each do |pagenum|
771
- # NOTE: addresses a very minor kerning issue for text adjacent to the comma
772
- if (prev_fragment = term_fragments[-1]).size == 1
773
- if ::String === pagenum
774
- term_fragments[-1] = prev_fragment.merge text: %(#{prev_fragment[:text]}, #{pagenum})
775
- next
776
- else
777
- term_fragments[-1] = prev_fragment.merge text: %(#{prev_fragment[:text]}, )
778
- end
749
+ if ::String === pagenum
750
+ term_fragments << ({ text: %(, #{pagenum}) })
779
751
  else
780
- term_fragments << ({ text: ', ' })
752
+ term_fragments << { text: ', ' }
753
+ term_fragments << pagenum
781
754
  end
782
- term_fragments << (::String === pagenum ? { text: pagenum } : pagenum)
783
755
  end
784
756
  end
785
757
  subterm_indent = @theme.description_list_description_indent
786
- typeset_formatted_text term_fragments, (calc_line_metrics @base_line_height), align: :left, color: @font_color, hanging_indent: subterm_indent * 2
758
+ typeset_formatted_text term_fragments, (calc_line_metrics @base_line_height), align: :left, color: @font_color, hanging_indent: subterm_indent * 2, consolidate: true
787
759
  indent subterm_indent do
788
760
  term.subterms.each do |subterm|
789
761
  convert_index_term subterm, pagenum_sequence_style
@@ -809,10 +781,7 @@ module Asciidoctor
809
781
  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)
810
782
  end if node.title?
811
783
  theme_font :abstract do
812
- prose_opts = { align: (@theme.abstract_text_align || @base_text_align).to_sym, hyphenate: true }
813
- if (text_indent = @theme.prose_text_indent) > 0
814
- prose_opts[:indent_paragraphs] = text_indent
815
- end
784
+ prose_opts = { align: (@theme.abstract_text_align || @base_text_align).to_sym, hyphenate: true, margin_bottom: 0 }
816
785
  # FIXME: allow theme to control more first line options
817
786
  if (line1_font_style = @theme.abstract_first_line_font_style&.to_sym) && line1_font_style != font_style
818
787
  case line1_font_style
@@ -833,14 +802,11 @@ module Asciidoctor
833
802
  prose_opts[:first_line_options] = first_line_options if first_line_options
834
803
  # FIXME: make this cleaner!!
835
804
  if node.blocks?
836
- last_block = node.last_child
837
805
  node.blocks.each do |child|
838
806
  if child.context == :paragraph
839
807
  child.document.playback_attributes child.attributes
840
- prose_opts[:margin_bottom] = 0 if child == last_block
841
- ink_prose child.content, ((text_align = resolve_text_align_from_role child.roles) ? (prose_opts.merge align: text_align) : prose_opts.dup)
808
+ convert_paragraph child, prose_opts.dup
842
809
  prose_opts.delete :first_line_options
843
- prose_opts.delete :margin_bottom
844
810
  else
845
811
  # FIXME: this could do strange things if the wrong kind of content shows up
846
812
  child.convert
@@ -850,7 +816,10 @@ module Asciidoctor
850
816
  if (text_align = resolve_text_align_from_role node.roles)
851
817
  prose_opts[:align] = text_align
852
818
  end
853
- ink_prose string, (prose_opts.merge margin_bottom: 0)
819
+ if IndentableTextAlignments[prose_opts[:align]] && (text_indent = @theme.prose_text_indent) > 0
820
+ prose_opts[:indent_paragraphs] = text_indent
821
+ end
822
+ ink_prose string, prose_opts
854
823
  end
855
824
  end
856
825
  end
@@ -859,16 +828,19 @@ module Asciidoctor
859
828
  theme_margin :block, :bottom, (next_enclosed_block node)
860
829
  end
861
830
 
862
- def convert_paragraph node
831
+ def convert_paragraph node, opts = nil
863
832
  add_dest_for_block node if node.id
864
833
 
865
- prose_opts = { margin_bottom: 0, hyphenate: true }
834
+ prose_opts = opts || { margin_bottom: 0, hyphenate: true }
866
835
  if (text_align = resolve_text_align_from_role (roles = node.roles), query_theme: true, remove_predefined: true)
867
836
  prose_opts[:align] = text_align
837
+ else
838
+ text_align = @base_text_align.to_sym
868
839
  end
869
840
  role_keys = roles.map {|role| %(role_#{role}) } unless roles.empty?
870
- if (text_indent = @theme.prose_text_indent) > 0 ||
871
- ((text_indent = @theme.prose_text_indent_inner) > 0 && node.previous_sibling&.context == :paragraph)
841
+ if IndentableTextAlignments[text_align] &&
842
+ ((text_indent = @theme.prose_text_indent) > 0 ||
843
+ ((text_indent = @theme.prose_text_indent_inner) > 0 && node.previous_sibling&.context == :paragraph))
872
844
  prose_opts[:indent_paragraphs] = text_indent
873
845
  end
874
846
  if (bottom_gutter = @bottom_gutters[-1][node])
@@ -915,7 +887,7 @@ module Asciidoctor
915
887
  label_width = label_min_width || (icon_size * 1.5)
916
888
  elsif (icon_path = has_icon || !(icon_path = (@theme[%(admonition_icon_#{type})] || {})[:image]) ?
917
889
  (get_icon_image_path node, type) :
918
- (ThemeLoader.resolve_theme_asset (apply_subs_discretely doc, icon_path, subs: [:attributes]), @themesdir)) &&
890
+ (ThemeLoader.resolve_theme_asset (apply_subs_discretely doc, icon_path, subs: [:attributes], imagesdir: @themesdir), @themesdir)) &&
919
891
  (::File.readable? icon_path)
920
892
  icons = true
921
893
  # TODO: introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
@@ -1073,7 +1045,7 @@ module Asciidoctor
1073
1045
  else
1074
1046
  highlighter = nil
1075
1047
  end
1076
- prev_subs = (subs = node.subs).dup
1048
+ saved_subs = (subs = node.subs).dup
1077
1049
  callouts_enabled = subs.include? :callouts
1078
1050
  highlight_idx = subs.index :highlight
1079
1051
  # NOTE: scratch? here only applies if listing block is nested inside another block
@@ -1083,16 +1055,33 @@ module Asciidoctor
1083
1055
  # switch the :highlight sub back to :specialcharacters
1084
1056
  subs[highlight_idx] = :specialcharacters
1085
1057
  else
1086
- prev_subs = nil
1058
+ saved_subs = nil
1087
1059
  end
1088
1060
  source_string = guard_indentation node.content
1089
1061
  elsif highlight_idx
1090
1062
  # NOTE: the source highlighter logic below handles the highlight and callouts subs
1091
- subs.replace subs - [:highlight, :callouts]
1092
- # NOTE: indentation guards will be added by the source highlighter logic
1093
- source_string = expand_tabs node.content
1063
+ if (subs - [:highlight, :callouts]).empty?
1064
+ subs.clear
1065
+ # NOTE: indentation guards will be added by the source highlighter logic
1066
+ source_string = expand_tabs node.content
1067
+ else
1068
+ if callouts_enabled
1069
+ saved_lines = node.lines.dup
1070
+ subs.delete :callouts
1071
+ prev_subs = subs.dup
1072
+ subs.clear
1073
+ source_string, conum_mapping = extract_conums node.content
1074
+ node.lines.replace (source_string.split LF)
1075
+ subs.replace prev_subs
1076
+ callouts_enabled = false
1077
+ end
1078
+ subs[highlight_idx] = :specialcharacters
1079
+ # NOTE: indentation guards will be added by the source highlighter logic
1080
+ source_string = expand_tabs unescape_xml (sanitize node.content, compact: false)
1081
+ node.lines.replace saved_lines if saved_lines
1082
+ end
1094
1083
  else
1095
- highlighter = prev_subs = nil
1084
+ highlighter = saved_subs = nil
1096
1085
  source_string = guard_indentation node.content
1097
1086
  end
1098
1087
  else
@@ -1184,7 +1173,7 @@ module Asciidoctor
1184
1173
  # NOTE: only format if we detect a need (callouts or inline formatting)
1185
1174
  source_chunks = (XMLMarkupRx.match? source_string) ? (text_formatter.format source_string) : [text: source_string]
1186
1175
  end
1187
- node.subs.replace prev_subs if prev_subs
1176
+ node.subs.replace saved_subs if saved_subs
1188
1177
  adjusted_font_size = ((node.option? 'autofit') || (node.document.attr? 'autofit-option')) ? (compute_autofit_font_size source_chunks, :code) : nil
1189
1178
  end
1190
1179
 
@@ -1308,7 +1297,7 @@ module Asciidoctor
1308
1297
  content = guard_indentation node.content
1309
1298
  ink_prose content,
1310
1299
  normalize: false,
1311
- align: :left,
1300
+ align: (resolve_text_align_from_role node.roles) || :left,
1312
1301
  hyphenate: true,
1313
1302
  margin_bottom: 0,
1314
1303
  bottom_gutter: (attribution ? nil : @bottom_gutters[-1][node])
@@ -1462,7 +1451,8 @@ module Asciidoctor
1462
1451
  end
1463
1452
  max_term_width += (term_padding[1] + term_padding[3])
1464
1453
  term_column_width = [max_term_width, bounds.width * 0.5].min
1465
- table table_data, position: :left, cell_style: { border_width: 0 }, column_widths: [term_column_width] do
1454
+ table table_data, position: :left, column_widths: [term_column_width] do
1455
+ cells.style border_width: 0
1466
1456
  @pdf.ink_table_caption node if node.title?
1467
1457
  end
1468
1458
  theme_margin :prose, :bottom, (next_enclosed_block actual_node) #unless actual_node.nested?
@@ -1631,6 +1621,9 @@ module Asciidoctor
1631
1621
  marker = node.parent.style == 'decimal' && index.abs < 10 ? %(#{index < 0 ? '-' : ''}0#{index.abs}.) : %(#{index}.)
1632
1622
  dir = (node.parent.option? 'reversed') ? :pred : :next
1633
1623
  @list_numerals << (index.public_send dir)
1624
+ [:font_color, :font_family, :font_size, :font_style, :line_height].each do |prop|
1625
+ marker_style[prop] = @theme[%(olist_marker_#{prop})] || marker_style[prop]
1626
+ end
1634
1627
  end
1635
1628
  end
1636
1629
  else # :ulist
@@ -1645,7 +1638,7 @@ module Asciidoctor
1645
1638
  else
1646
1639
  marker = @theme[%(ulist_marker_#{marker_type}_content)] || Bullets[marker_type]
1647
1640
  end
1648
- [:font_color, :font_family, :font_size, :line_height].each do |prop|
1641
+ [:font_color, :font_family, :font_size, :font_style, :line_height].each do |prop|
1649
1642
  marker_style[prop] = @theme[%(ulist_marker_#{marker_type}_#{prop})] || @theme[%(ulist_marker_#{prop})] || marker_style[prop]
1650
1643
  end if marker
1651
1644
  end
@@ -1656,8 +1649,9 @@ module Asciidoctor
1656
1649
  log :info, 'deprecated fa icon set found in theme; use fas, far, or fab instead'
1657
1650
  marker_style[:font_family] = FontAwesomeIconSets.find {|candidate| (icon_font_data candidate).yaml[candidate].value? marker } || 'fas'
1658
1651
  end
1652
+ marker_style[:font_style] &&= marker_style[:font_style].to_sym
1659
1653
  marker_gap = rendered_width_of_char 'x'
1660
- font marker_style[:font_family], marker_style[:font_size] do
1654
+ font marker_style[:font_family], size: marker_style[:font_size], style: marker_style[:font_style] do
1661
1655
  marker_width = rendered_width_of_string marker
1662
1656
  # NOTE: compensate if character_spacing is not applied to first character
1663
1657
  # see https://github.com/prawnpdf/prawn/commit/c61c5d48841910aa11b9e3d6f0e01b68ce435329
@@ -1676,6 +1670,7 @@ module Asciidoctor
1676
1670
  color: marker_style[:font_color],
1677
1671
  inline_format: false,
1678
1672
  line_height: marker_style[:line_height],
1673
+ style: marker_style[:font_style],
1679
1674
  margin: 0,
1680
1675
  normalize: false,
1681
1676
  single_line: true
@@ -1703,7 +1698,7 @@ module Asciidoctor
1703
1698
  alignment = float_to.to_sym
1704
1699
  elsif (alignment = node.attr 'align')
1705
1700
  alignment = (BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left
1706
- elsif !(alignment = node.roles.reverse.find {|r| BlockAlignmentNames.include? r }&.to_sym)
1701
+ elsif !(alignment = node.roles.reverse.find {|role| BlockAlignmentNames.include? role }&.to_sym)
1707
1702
  alignment = @theme.image_align&.to_sym || :left
1708
1703
  end
1709
1704
  end
@@ -1937,14 +1932,16 @@ module Asciidoctor
1937
1932
  page_layout = nil
1938
1933
  end
1939
1934
 
1940
- if at_page_top?
1935
+ if at_page_top? && !(node.option? 'always')
1941
1936
  if page_layout && page_layout != page.layout && page.empty?
1942
1937
  delete_current_page
1943
- advance_page layout: page_layout
1938
+ advance_page layout: page_layout, margin: @page_margin[page_layout][page_side nil, @folio_placement[:inverted]]
1944
1939
  end
1945
1940
  elsif page_layout
1946
- advance_page layout: page_layout
1941
+ bounds.current_column = bounds.last_column if ColumnBox === bounds && !(node.has_role? 'column')
1942
+ advance_page layout: page_layout, margin: @page_margin[page_layout][page_side nil, @folio_placement[:inverted]]
1947
1943
  else
1944
+ bounds.current_column = bounds.last_column if ColumnBox === bounds && !(node.has_role? 'column')
1948
1945
  advance_page
1949
1946
  end
1950
1947
  end
@@ -2217,7 +2214,7 @@ module Asciidoctor
2217
2214
 
2218
2215
  if node.option? 'autowidth'
2219
2216
  table_width = (node.attr? 'width') ? bounds.width * ((node.attr 'tablepcwidth') / 100.0) :
2220
- (((node.has_role? 'stretch') || (node.has_role? 'spread')) ? bounds.width : nil)
2217
+ (((node.has_role? 'stretch')) ? bounds.width : nil)
2221
2218
  column_widths = []
2222
2219
  else
2223
2220
  table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
@@ -2239,10 +2236,10 @@ module Asciidoctor
2239
2236
  # NOTE: position is handled by this method
2240
2237
  position: :left,
2241
2238
  # NOTE: the border color, style, and width of the outer frame is set in the table callback block
2242
- cell_style: { border_color: grid_color.values, border_lines: grid_style.values, border_width: grid_width.values },
2243
2239
  width: table_width,
2244
2240
  column_widths: column_widths,
2245
2241
  }
2242
+ cell_style = { border_color: grid_color.values, border_lines: grid_style.values, border_width: grid_width.values }
2246
2243
 
2247
2244
  # QUESTION: should we support nth; should we support sequence of roles?
2248
2245
  case node.attr 'stripes', nil, 'table-stripes'
@@ -2258,6 +2255,8 @@ module Asciidoctor
2258
2255
 
2259
2256
  left_padding = right_padding = nil
2260
2257
  table table_data, table_settings do
2258
+ # NOTE: cell_style must be applied manually to be compatible with both prawn-table 0.2.2 and prawn-table 0.2.3
2259
+ cells.style cell_style
2261
2260
  @column_widths = column_widths unless column_widths.empty?
2262
2261
  # NOTE: call width to capture resolved table width
2263
2262
  table_width = width
@@ -2381,6 +2380,9 @@ module Asciidoctor
2381
2380
  node.content
2382
2381
  elsif node.content_model != :compound && (string = node.content)
2383
2382
  prose_opts = opts.merge hyphenate: true, margin_bottom: 0
2383
+ if (text_align = resolve_text_align_from_role node.roles)
2384
+ prose_opts[:align] = text_align
2385
+ end
2384
2386
  if (bottom_gutter = @bottom_gutters[-1][node])
2385
2387
  prose_opts[:bottom_gutter] = bottom_gutter
2386
2388
  end
@@ -2429,7 +2431,7 @@ module Asciidoctor
2429
2431
  # QUESTION: should we insert breakable chars into URI when building fragment instead?
2430
2432
  %(#{anchor}<a href="#{target}"#{attrs.join}>#{breakable_uri text}</a>)
2431
2433
  # NOTE: @media may not be initialized if method is called before convert phase
2432
- elsif (doc.attr? 'show-link-uri') || !(@media == 'screen' || (doc.attribute_locked? 'show-link-uri') || ((doc.instance_variable_get :@attributes_modified).include? 'show-link-uri'))
2434
+ elsif (doc.attr? 'show-link-uri') || (@media != 'screen' && (doc.attr_unspecified? 'show-link-uri'))
2433
2435
  # QUESTION: should we insert breakable chars into URI when building fragment instead?
2434
2436
  # TODO: allow style of printed link to be controlled by theme
2435
2437
  %(#{anchor}<a href="#{target}"#{attrs.join}>#{text}</a> [<font size="0.85em">#{breakable_uri bare_target}</font>&#93;)
@@ -2496,9 +2498,9 @@ module Asciidoctor
2496
2498
  else
2497
2499
  label = index
2498
2500
  end
2499
- %(#{anchor}<sup>[<a anchor="_footnotedef_#{index}">#{label}</a>]</sup>)
2501
+ %(<sup class="wj">#{anchor}[<a anchor="_footnotedef_#{index}">#{label}</a>]</sup>)
2500
2502
  elsif node.type == :xref
2501
- %(<sup><font color="#{theme.role_unresolved_font_color}">[#{node.text}]</font></sup>)
2503
+ %(<sup class="wj"><font color="#{theme.role_unresolved_font_color}">[#{node.text}]</font></sup>)
2502
2504
  else
2503
2505
  log :warn, %(unknown footnote type: #{node.type.inspect})
2504
2506
  nil
@@ -2793,6 +2795,11 @@ module Asciidoctor
2793
2795
  if (imagesdir = opts[:imagesdir])
2794
2796
  imagesdir_to_restore = doc.attr 'imagesdir'
2795
2797
  doc.set_attr 'imagesdir', imagesdir
2798
+ remove_docimagesdir = doc.set_attr 'docimagesdir', (::File.absolute_path imagesdir_to_restore.to_s, (doc.attr 'docdir', '')), false
2799
+ end
2800
+ if (page_layout = opts[:page_layout])
2801
+ page_layout_to_restore = doc.attr 'page-layout'
2802
+ doc.set_attr 'page-layout', page.layout.to_s
2796
2803
  end
2797
2804
  # FIXME: get sub_attributes to handle drop-line w/o a warning
2798
2805
  doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
@@ -2801,20 +2808,26 @@ module Asciidoctor
2801
2808
  value = (value.split LF).delete_if {|line| SimpleAttributeRefRx.match? line }.join LF if opts[:drop_lines_with_unresolved_attributes] && (value.include? '{')
2802
2809
  value = value.gsub '\{', '{' if escaped_attr_ref
2803
2810
  doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
2804
- imagesdir_to_restore ? (doc.set_attr 'imagesdir', imagesdir_to_restore) : (doc.remove_attr 'imagesdir') if imagesdir
2811
+ page_layout_to_restore ? (doc.set_attr 'page-layout', page_layout_to_restore) : (doc.remove_attr 'page-layout') if page_layout
2812
+ if imagesdir
2813
+ imagesdir_to_restore ? (doc.set_attr 'imagesdir', imagesdir_to_restore) : (doc.remove_attr 'imagesdir')
2814
+ doc.remove_attr 'docimagesdir' if remove_docimagesdir
2815
+ end
2805
2816
  value
2806
2817
  end
2807
2818
 
2808
2819
  # Position the cursor for where to ink the specified section title or discrete heading node.
2809
2820
  #
2810
- # This method computes whether there is enough room on the page to prevent the specified node
2811
- # from being orphaned. If there is not enough room, the method will advance the cursor to
2821
+ # This method computes whether there's enough room on the page to prevent the specified node
2822
+ # from being orphaned. If there's not enough room, the method will advance the cursor to
2812
2823
  # the next page. This method is not called if the cursor is already at the top of the page or
2813
2824
  # whether this node has no node that follows it in document order.
2814
2825
  def arrange_heading node, title, opts
2815
- if node.option? 'breakable'
2826
+ if (min_height_after = @theme.heading_min_height_after) == 'auto' || (node.option? 'breakable')
2816
2827
  orphaned = nil
2828
+ doc = node.document
2817
2829
  dry_run single_page: true do
2830
+ push_scratch doc
2818
2831
  start_page = page
2819
2832
  theme_font :heading, level: opts[:level] do
2820
2833
  if opts[:part]
@@ -2829,19 +2842,18 @@ module Asciidoctor
2829
2842
  page.tare_content_stream
2830
2843
  orphaned = stop_if_first_page_empty { node.context == :section ? (traverse node) : (convert node.next_sibling) }
2831
2844
  end
2845
+ ensure
2846
+ pop_scratch doc
2832
2847
  end
2833
2848
  advance_page if orphaned
2834
2849
  else
2835
2850
  theme_font :heading, level: (hlevel = opts[:level]) do
2836
2851
  h_padding_t, h_padding_r, h_padding_b, h_padding_l = expand_padding_value @theme[%(heading_h#{hlevel}_padding)]
2837
2852
  h_fits = indent h_padding_l, h_padding_r do
2838
- # FIXME: this height doesn't account for impact of text transform or inline formatting
2839
- heading_h = (height_of_typeset_text title) +
2853
+ heading_h = (height_of_typeset_text title, inline_format: true, text_transform: @text_transform) +
2840
2854
  (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
2841
2855
  (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom) + h_padding_t + h_padding_b
2842
- if (min_height_after = @theme.heading_min_height_after) && (node.context == :section ? node.blocks? : !node.last_child?)
2843
- heading_h += min_height_after
2844
- end
2856
+ heading_h += min_height_after if min_height_after && (node.context == :section ? node.blocks? : !node.last_child?)
2845
2857
  cursor >= heading_h
2846
2858
  end
2847
2859
  advance_page unless h_fits
@@ -3010,8 +3022,16 @@ module Asciidoctor
3010
3022
  end
3011
3023
 
3012
3024
  def height_of_typeset_text string, opts = {}
3025
+ if (transform = opts[:text_transform])
3026
+ string = transform_text string, transform
3027
+ end
3028
+ if (inline_format = opts[:inline_format]) && (InlineFormatSniffRx.match? string)
3029
+ fragments = parse_text string, inline_format: inline_format
3030
+ else
3031
+ fragments = [{ text: string }]
3032
+ end
3013
3033
  line_metrics = (calc_line_metrics opts[:line_height] || @base_line_height)
3014
- (height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + (opts[:single_line] ? 0 : line_metrics.padding_bottom)
3034
+ (height_of_formatted fragments, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + (opts[:single_line] ? 0 : line_metrics.padding_bottom)
3015
3035
  end
3016
3036
 
3017
3037
  # Render the caption in the current document. If the dry_run option is true, return the height.
@@ -3103,7 +3123,7 @@ module Asciidoctor
3103
3123
  opts = opts.merge inherited
3104
3124
  end
3105
3125
  unless scratch? || !(bg_color = @theme[%(#{category_caption}_background_color)] || @theme.caption_background_color)
3106
- caption_height = height_of_typeset_text string
3126
+ caption_height = height_of_typeset_text string, inline_format: true, text_transform: @text_transform
3107
3127
  fill_at = [bounds.left, cursor]
3108
3128
  fill_at[1] -= (margin[:top] || 0) unless at_page_top?
3109
3129
  float { bounding_box(fill_at, width: container_width, height: caption_height) { fill_bounds bg_color } }
@@ -3143,7 +3163,7 @@ module Asciidoctor
3143
3163
  open_graphics_state if face == :front
3144
3164
  return
3145
3165
  elsif image_path == '~'
3146
- @page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress'
3166
+ @page_margin[:cover] = @page_margin[page.layout][:recto] if @media == 'prepress'
3147
3167
  return
3148
3168
  end
3149
3169
 
@@ -3164,9 +3184,9 @@ module Asciidoctor
3164
3184
  def ink_footnotes node
3165
3185
  return if (fns = (doc = node.document).footnotes - @rendered_footnotes).empty?
3166
3186
  theme_margin :block, :bottom if node.context == :document || node == node.document.last_child
3167
- theme_margin :footnotes, :top
3187
+ theme_margin :footnotes, :top unless (valign_bottom = @theme.footnotes_margin_top == 'auto')
3168
3188
  with_dry_run do |extent|
3169
- if (single_page_height = extent&.single_page_height) && (delta = cursor - single_page_height - 0.0001) > 0
3189
+ if valign_bottom && (single_page_height = extent&.single_page_height) && (delta = cursor - single_page_height - 0.0001) > 0
3170
3190
  move_down delta
3171
3191
  end
3172
3192
  theme_font :footnotes do
@@ -3274,7 +3294,7 @@ module Asciidoctor
3274
3294
 
3275
3295
  def allocate_running_content_layout doc, page, periphery, cache
3276
3296
  cache[layout = page.layout] ||= begin
3277
- page_margin_recto = @page_margin_by_side[:recto]
3297
+ page_margin_recto = @page_margin[layout][:recto]
3278
3298
  trim_margin_recto = @theme[%(#{periphery}_recto_margin)] || @theme[%(#{periphery}_margin)] || [0, 'inherit', 0, 'inherit']
3279
3299
  trim_margin_recto = (expand_margin_value trim_margin_recto).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_recto[i] : v.to_f }
3280
3300
  trim_content_margin_recto = @theme[%(#{periphery}_recto_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
@@ -3284,7 +3304,7 @@ module Asciidoctor
3284
3304
  else
3285
3305
  trim_padding_recto = trim_content_margin_recto
3286
3306
  end
3287
- page_margin_verso = @page_margin_by_side[:verso]
3307
+ page_margin_verso = @page_margin[layout][:verso]
3288
3308
  trim_margin_verso = @theme[%(#{periphery}_verso_margin)] || @theme[%(#{periphery}_margin)] || [0, 'inherit', 0, 'inherit']
3289
3309
  trim_margin_verso = (expand_margin_value trim_margin_verso).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_verso[i] : v.to_f }
3290
3310
  trim_content_margin_verso = @theme[%(#{periphery}_verso_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
@@ -3679,7 +3699,7 @@ module Asciidoctor
3679
3699
  logo_image_attrs = (AttributeList.new $2).parse %w(alt width height)
3680
3700
  if logo_image_from_theme
3681
3701
  relative_to_imagesdir = false
3682
- logo_image_path = apply_subs_discretely doc, $1, subs: [:attributes]
3702
+ logo_image_path = apply_subs_discretely doc, $1, subs: [:attributes], imagesdir: @themesdir
3683
3703
  logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
3684
3704
  else
3685
3705
  relative_to_imagesdir = true
@@ -3689,7 +3709,7 @@ module Asciidoctor
3689
3709
  logo_image_attrs = {}
3690
3710
  relative_to_imagesdir = false
3691
3711
  if logo_image_from_theme
3692
- logo_image_path = apply_subs_discretely doc, logo_image_path, subs: [:attributes]
3712
+ logo_image_path = apply_subs_discretely doc, logo_image_path, subs: [:attributes], imagesdir: @themesdir
3693
3713
  logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
3694
3714
  end
3695
3715
  end
@@ -3775,12 +3795,12 @@ module Asciidoctor
3775
3795
  end
3776
3796
  end
3777
3797
 
3778
- def allocate_toc doc, toc_num_levels, toc_start_cursor, title_page_on
3798
+ def allocate_toc doc, toc_num_levels, toc_start_cursor, break_after_toc
3779
3799
  toc_start_page_number = page_number
3780
3800
  to_page = nil
3781
3801
  extent = dry_run onto: self do
3782
3802
  to_page = (ink_toc doc, toc_num_levels, toc_start_page_number, toc_start_cursor).end
3783
- theme_margin :block, :bottom unless title_page_on
3803
+ theme_margin :block, :bottom unless break_after_toc
3784
3804
  end
3785
3805
  # NOTE: patch for custom converters that allocate extra TOC pages without actually creating them
3786
3806
  if to_page > extent.to.page
@@ -3788,7 +3808,7 @@ module Asciidoctor
3788
3808
  extent.to.cursor = bounds.height
3789
3809
  end
3790
3810
  # NOTE: reserve pages for the toc; leaves cursor on page after last page in toc
3791
- if title_page_on
3811
+ if break_after_toc
3792
3812
  extent.each_page { start_new_page }
3793
3813
  else
3794
3814
  extent.each_page {|first_page| start_new_page unless first_page }
@@ -4059,18 +4079,14 @@ module Asciidoctor
4059
4079
  return []
4060
4080
  elsif (image_path.include? ':') && image_path =~ ImageAttributeValueRx
4061
4081
  image_attrs = (AttributeList.new $2).parse %w(alt width)
4062
- if from_theme
4063
- image_path = apply_subs_discretely doc, $1, subs: [:attributes]
4064
- image_relative_to = @themesdir
4065
- else
4066
- image_path = $1
4067
- image_relative_to = true
4068
- end
4069
- elsif from_theme
4070
- image_path = apply_subs_discretely doc, image_path, subs: [:attributes]
4071
- image_relative_to = @themesdir
4082
+ image_path = $1
4083
+ image_relative_to = true
4084
+ end
4085
+ if from_theme
4086
+ image_path = apply_subs_discretely doc, image_path, subs: [:attributes], imagesdir: (image_relative_to = @themesdir), page_layout: page.layout.to_s
4087
+ elsif image_path.include? '{page-layout}'
4088
+ image_path = image_path.sub '{page-layout}', page.layout.to_s
4072
4089
  end
4073
-
4074
4090
  image_path, image_format = ::Asciidoctor::Image.target_and_format image_path, image_attrs
4075
4091
  image_path = resolve_image_path doc, image_path, image_format, image_relative_to
4076
4092
 
@@ -4284,9 +4300,30 @@ module Asciidoctor
4284
4300
  end
4285
4301
  end
4286
4302
 
4303
+ def resolve_page_margin value
4304
+ return if value.nil_or_empty?
4305
+ case value
4306
+ when ::Array
4307
+ value = value.slice 0, 4 if value.length > 4
4308
+ value.map {|v| ::Numeric === v ? v : (str_to_pt v.to_s) }
4309
+ when ::Numeric
4310
+ [value]
4311
+ when ::String
4312
+ if (value.start_with? '[') && (value.end_with? ']')
4313
+ return if (value = (value.slice 1, value.length - 2).rstrip).empty?
4314
+ if (value = value.split ',', -1).length > 4
4315
+ value = value.slice 0, 4
4316
+ end
4317
+ value.map {|v| str_to_pt v.rstrip }
4318
+ else
4319
+ [(str_to_pt value)]
4320
+ end
4321
+ end
4322
+ end
4323
+
4287
4324
  def resolve_text_align_from_role roles, query_theme: false, remove_predefined: false
4288
- if (align_role = roles.reverse.find {|r| TextAlignmentRoles.include? r })
4289
- roles.replace roles - TextAlignmentRoles if remove_predefined
4325
+ if (align_role = roles.reverse.find {|role| TextAlignmentRoles[role] })
4326
+ roles.replace roles - TextAlignmentRoles.keys if remove_predefined
4290
4327
  (align_role.slice 5, align_role.length).to_sym
4291
4328
  elsif query_theme
4292
4329
  roles.reverse.each do |role|
@@ -4336,17 +4373,17 @@ module Asciidoctor
4336
4373
  elsif @ppbook && page_number > 0 && recto_page?
4337
4374
  start_new_page
4338
4375
  end
4339
- side = page_side (recycle ? nil : page_number + 1), @folio_placement[:inverted]
4340
- prev_bg_image = @page_bg_image[side]
4341
- prev_bg_color = @page_bg_color
4342
4376
  if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
4343
- @page_bg_image[side] = bg_image[0] && bg_image
4377
+ side = page_side (recycle ? nil : page_number + 1), @folio_placement[:inverted]
4378
+ prev_bg_image = get_page_bg_image doc, @theme, (layout = page.layout), side
4379
+ @page_bg_image[layout][side] = bg_image[0] && bg_image
4344
4380
  end
4345
4381
  if (bg_color = resolve_theme_color :title_page_background_color)
4382
+ prev_bg_color = @page_bg_color
4346
4383
  @page_bg_color = bg_color
4347
4384
  end
4348
- recycle ? float { init_page self } : start_new_page
4349
- @page_bg_image[side] = prev_bg_image if bg_image
4385
+ recycle ? float { init_page doc, self } : start_new_page
4386
+ @page_bg_image[layout][side] = prev_bg_image if bg_image
4350
4387
  @page_bg_color = prev_bg_color if bg_color
4351
4388
  true
4352
4389
  end
@@ -4433,7 +4470,12 @@ module Asciidoctor
4433
4470
  if opts.key? :level
4434
4471
  hlevel_category = %(#{category}_h#{opts[:level]})
4435
4472
  family = @theme[%(#{hlevel_category}_font_family)] || @theme[%(#{category}_font_family)] || @theme.base_font_family || font_family
4436
- size = (@theme[%(#{hlevel_category}_font_size)] || @theme[%(#{category}_font_size)] || @root_font_size) * @font_scale
4473
+ if (size = @theme[%(#{hlevel_category}_font_size)] || @theme[%(#{category}_font_size)])
4474
+ scale = @font_scale unless ::String === size
4475
+ else
4476
+ scale = @font_scale
4477
+ size = @root_font_size
4478
+ end
4437
4479
  style = @theme[%(#{hlevel_category}_font_style)] || @theme[%(#{category}_font_style)]
4438
4480
  color = @theme[%(#{hlevel_category}_font_color)] || @theme[%(#{category}_font_color)]
4439
4481
  kerning = resolve_font_kerning @theme[%(#{hlevel_category}_font_kerning)] || @theme[%(#{category}_font_kerning)]
@@ -4443,7 +4485,11 @@ module Asciidoctor
4443
4485
  else
4444
4486
  inherited_font = font_info
4445
4487
  family = @theme[%(#{category}_font_family)] || inherited_font[:family]
4446
- size = (size = @theme[%(#{category}_font_size)]) ? size * @font_scale : inherited_font[:size]
4488
+ if (size = @theme[%(#{category}_font_size)])
4489
+ scale = @font_scale unless ::String === size
4490
+ else
4491
+ size = inherited_font[:size]
4492
+ end
4447
4493
  style = @theme[%(#{category}_font_style)] || inherited_font[:style]
4448
4494
  color = @theme[%(#{category}_font_color)]
4449
4495
  kerning = resolve_font_kerning @theme[%(#{category}_font_kerning)]
@@ -4459,6 +4505,7 @@ module Asciidoctor
4459
4505
 
4460
4506
  result = nil
4461
4507
  font family, size: size, style: style&.to_sym do
4508
+ @font_size *= scale if scale
4462
4509
  result = yield
4463
4510
  ensure
4464
4511
  @font_color = prev_color if color
@@ -4518,6 +4565,7 @@ module Asciidoctor
4518
4565
  # QUESTION: combine with typeset_text?
4519
4566
  def typeset_formatted_text fragments, line_metrics, opts = {}
4520
4567
  opts = { leading: line_metrics.leading, initial_gap: line_metrics.padding_top, final_gap: line_metrics.final_gap }.merge opts
4568
+ fragments = consolidate_fragments fragments if opts.delete :consolidate
4521
4569
  if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
4522
4570
  indent hanging_indent do
4523
4571
  formatted_text fragments, (opts.merge indent_paragraphs: -hanging_indent)
@@ -4727,6 +4775,20 @@ module Asciidoctor
4727
4775
  end
4728
4776
  end
4729
4777
 
4778
+ def consolidate_fragments fragments
4779
+ return fragments unless fragments.size > 1
4780
+ accum = []
4781
+ prev_fragment = nil
4782
+ fragments.each do |fragment|
4783
+ if prev_fragment && fragment == (prev_fragment.merge text: (fragment_text = fragment[:text]))
4784
+ prev_fragment[:text] += fragment_text
4785
+ else
4786
+ accum << (prev_fragment = fragment)
4787
+ end
4788
+ end
4789
+ accum
4790
+ end
4791
+
4730
4792
  def conum_glyph number
4731
4793
  @conum_glyphs[number - 1]
4732
4794
  end
@@ -4795,6 +4857,23 @@ module Asciidoctor
4795
4857
  (code.start_with? '\u') ? ([((code.slice 2, code.length).to_i 16)].pack 'U1') : code
4796
4858
  end
4797
4859
 
4860
+ def get_page_bg_image doc, theme_, layout, side
4861
+ (@page_bg_image[layout] ||= begin
4862
+ if (bg_image = resolve_background_image doc, theme_, 'page-background-image')&.first
4863
+ val = { verso: bg_image, recto: bg_image }
4864
+ else
4865
+ val = { verso: nil, recto: nil }
4866
+ end
4867
+ if (bg_image = resolve_background_image doc, theme_, 'page-background-image-verso')
4868
+ val[:verso] = bg_image[0] && bg_image
4869
+ end
4870
+ if (bg_image = resolve_background_image doc, theme_, 'page-background-image-recto')
4871
+ val[:recto] = bg_image[0] && bg_image
4872
+ end
4873
+ val
4874
+ end)[side]
4875
+ end
4876
+
4798
4877
  def get_icon_image_path node, type, resolve = true
4799
4878
  doc = node.document
4800
4879
  doc.remove_attr 'data-uri' if (data_uri_enabled = doc.attr? 'data-uri')
@@ -4818,18 +4897,17 @@ module Asciidoctor
4818
4897
  @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 }
4819
4898
  end
4820
4899
 
4821
- # NOTE: init_page is called within a float context; this will suppress prawn-svg messing with the cursor
4822
4900
  # NOTE: init_page is not called for imported pages, cover pages, image pages, and pages in the scratch document
4823
- def init_page *_args
4901
+ def init_page doc, _self
4824
4902
  next_page_side = page_side nil, @folio_placement[:inverted]
4825
- if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_number == 1 ? :cover : next_page_side]) != page_margin
4903
+ if @media == 'prepress' && (next_page_margin = page_number == 1 ? @page_margin[:cover] : @page_margin[page.layout][next_page_side]) != page_margin
4826
4904
  set_page_margin next_page_margin
4827
4905
  end
4828
4906
  unless @page_bg_color == 'FFFFFF'
4829
4907
  fill_absolute_bounds @page_bg_color
4830
4908
  tare = true
4831
4909
  end
4832
- if (bg_image_path, bg_image_opts = @page_bg_image[next_page_side])
4910
+ if (bg_image_path, bg_image_opts = get_page_bg_image doc, @theme, (layout = page.layout), next_page_side)
4833
4911
  begin
4834
4912
  if bg_image_opts[:format] == 'pdf'
4835
4913
  # NOTE: pages that use PDF for the background do not support a background color or running content
@@ -4840,9 +4918,10 @@ module Asciidoctor
4840
4918
  end
4841
4919
  tare = true
4842
4920
  rescue
4843
- facing_page_side = (PageSides - [next_page_side])[0]
4844
- @page_bg_image[facing_page_side] = nil if @page_bg_image[facing_page_side] == @page_bg_image[next_page_side]
4845
- @page_bg_image[next_page_side] = nil
4921
+ facing_page_side = PageSides[(PageSides.index next_page_side) - 1]
4922
+ bg_image_by_side = @page_bg_image[layout]
4923
+ bg_image_by_side[facing_page_side] = nil if bg_image_by_side[facing_page_side] == bg_image_by_side[next_page_side]
4924
+ bg_image_by_side[next_page_side] = nil
4846
4925
  log :warn, %(could not embed page background image: #{bg_image_path}; #{$!.message})
4847
4926
  end
4848
4927
  end
@@ -5013,7 +5092,9 @@ module Asciidoctor
5013
5092
  end
5014
5093
 
5015
5094
  def resolve_top val
5016
- if val.end_with? 'vh'
5095
+ if ::Numeric === val
5096
+ @y - val
5097
+ elsif val.end_with? 'vh'
5017
5098
  page_height * (1 - (val.to_f / 100))
5018
5099
  elsif val.end_with? '%'
5019
5100
  @y - effective_page_height * (val.to_f / 100)
@@ -5105,20 +5186,17 @@ module Asciidoctor
5105
5186
  @label = :scratch
5106
5187
  @save_state = nil
5107
5188
  @scratch_depth = 0
5108
- # NOTE: don't need background image in scratch document; can cause marshal error anyway
5109
- saved_page_bg_image, @page_bg_image = @page_bg_image, { verso: nil, recto: nil }
5110
5189
  # NOTE: pdfmark has a reference to the Asciidoctor::Document, which we don't want to serialize
5111
5190
  saved_pdfmark, @pdfmark = @pdfmark, nil
5112
5191
  # IMPORTANT: don't set font before using marshal as it causes serialization to fail
5113
5192
  result = yield
5114
5193
  @pdfmark = saved_pdfmark
5115
- @page_bg_image = saved_page_bg_image
5116
5194
  @label = :primary
5117
5195
  result
5118
5196
  end
5119
5197
 
5120
5198
  def init_scratch originator
5121
- if @media == 'prepress' && page_margin != (page_margin_recto = @page_margin_by_side[:recto])
5199
+ if @media == 'prepress' && page_margin != (page_margin_recto = @page_margin[page.layout][:recto])
5122
5200
  # NOTE: prepare scratch document to use page margin from recto side (which has same width as verso side)
5123
5201
  set_page_margin page_margin_recto
5124
5202
  end