Module: Asciidoctor::Prawn::Extensions

Includes:
Asciidoctor::PDF::Measurements, Asciidoctor::PDF::Sanitizer, Asciidoctor::PDF::TextTransformer
Included in:
Asciidoctor::PDF::Converter
Defined in:
lib/asciidoctor/pdf/ext/prawn/extensions.rb

Defined Under Namespace

Classes: Extent, LineMetrics, Position, ScratchExtent

Constant Summary collapse

ColumnBox =
::Prawn::Document::ColumnBox
FontAwesomeIconSets =
%w(fab far fas)
IconSets =
%w(fab far fas fi mdi).to_set
IconSetPrefixes =
IconSets.map {|it| it + '-' }
InitialPageContent =
%(q\n)
PlaceholderChar =

NOTE: must use a visible char for placeholder or else Prawn won’t reserve space for the fragment

?\u2063
NewPageRequiredError =
::Class.new ::StopIteration
InhibitNewPageProc =
proc do |pdf|
  pdf.delete_current_page
  raise NewPageRequiredError
end
DetectEmptyFirstPage =
::Module.new
DetectEmptyFirstPageProc =
proc do |delegate, pdf|
  if pdf.state.pages[pdf.page_number - 2].empty?
    pdf.delete_current_page
    raise NewPageRequiredError
  end
  delegate.call pdf if (pdf.state.on_page_create_callback = delegate)
end

Constants included from Asciidoctor::PDF::TextTransformer

Asciidoctor::PDF::TextTransformer::ContiguousCharsRx, Asciidoctor::PDF::TextTransformer::Hyphen, Asciidoctor::PDF::TextTransformer::LowerAlphaChars, Asciidoctor::PDF::TextTransformer::PCDATAFilterRx, Asciidoctor::PDF::TextTransformer::SmallCapsChars, Asciidoctor::PDF::TextTransformer::SoftHyphen, Asciidoctor::PDF::TextTransformer::TagFilterRx, Asciidoctor::PDF::TextTransformer::WordRx, Asciidoctor::PDF::TextTransformer::XMLMarkupRx

Constants included from Asciidoctor::PDF::Sanitizer

Asciidoctor::PDF::Sanitizer::CharRefRx, Asciidoctor::PDF::Sanitizer::InverseXMLSpecialChars, Asciidoctor::PDF::Sanitizer::InverseXMLSpecialCharsRx, Asciidoctor::PDF::Sanitizer::SanitizeXMLRx, Asciidoctor::PDF::Sanitizer::UnescapedAmpersandRx, Asciidoctor::PDF::Sanitizer::XMLSpecialChars, Asciidoctor::PDF::Sanitizer::XMLSpecialCharsRx

Constants included from Asciidoctor::PDF::Measurements

Asciidoctor::PDF::Measurements::InsetMeasurementValueRx, Asciidoctor::PDF::Measurements::MeasurementValueHintRx, Asciidoctor::PDF::Measurements::MeasurementValueRx

Instance Method Summary collapse

Methods included from Asciidoctor::PDF::TextTransformer

#capitalize_words, #capitalize_words_pcdata, #hyphenate_words, #hyphenate_words_pcdata, #lowercase_pcdata, #smallcaps_pcdata, #transform_text, #uppercase_pcdata

Methods included from Asciidoctor::PDF::Sanitizer

#encode_quotes, #escape_amp, #escape_xml, #sanitize, #unescape_xml

Methods included from Asciidoctor::PDF::Measurements

#resolve_measurement_values, #str_to_pt, #to_pt

Instance Method Details

#advance_page(options = {}) ⇒ Object

This method is a smarter version of start_new_page. It only calls start_new_page options are specified and the current page is the last page in the document. Otherwise, it advances the cursor to the next page (or column) using Bounds#move_past_bottom.



943
944
945
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 943

def advance_page options = {}
  options.empty? || !last_page? ? bounds.move_past_bottom : (start_new_page options)
end

#allocate_scratch_prototypeObject

Scratch



955
956
957
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 955

def allocate_scratch_prototype
  @scratch_prototype = create_scratch_prototype { ::Marshal.load ::Marshal.dump self }
end

#arrange_block(node, &block) ⇒ Object

Yields to the specified block multiple times, first to determine where to render the content so it fits properly, then once more, this time providing access to the content’s extent, to ink the content in the primary document.

This method yields to the specified block in a scratch document by calling dry_run to determine where the content should start in the primary document. In the process, it also computes the extent of the content. It then returns to the primary document and yields to the block again, this time passing in the extent of the content. The extent can be used to draw a border and/or background under the content before inking it.

This method is intended to enclose the conversion of a single content block, such as a sidebar or example block. The arrange logic attempts to keep unbreakable content on the same page, keeps the top caption pinned to the top of the content, computes the extent of the content for the purpose of drawing a border and/or background underneath it, and ensures that the extent does not begin near the bottom of a page if the first line of content doesn’t fit. If unbreakable content does not fit on a single page, the content is treated as breakable.

The block passed to this method should use advance_page to move to the next page rather than start_new_page. Using start_new_page can mangle the calculation of content block’s extent.



992
993
994
995
996
997
998
999
1000
1001
1002
1003
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 992

def arrange_block node, &block
  keep_together = (node.option? 'unbreakable') && !at_page_top?
  doc = node.document
  block_for_scratch = proc do
    push_scratch doc
    instance_exec(&block)
  ensure
    pop_scratch doc
  end
  extent = dry_run keep_together: keep_together, onto: [self, keep_together], &block_for_scratch
  scratch? ? block_for_scratch.call : (yield extent)
end

#at_page_top?Boolean

Returns whether the cursor is at the top of the page (i.e., margin box).

Returns:

  • (Boolean)


250
251
252
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 250

def at_page_top?
  @y == (ColumnBox === bounds ? bounds : @margin_box).absolute_top
end

#bounds_margin_leftObject

Returns the total left margin (to the page edge) for the current bounds.



216
217
218
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 216

def bounds_margin_left
  bounds.absolute_left
end

#bounds_margin_rightObject

Returns the total right margin (to the page edge) for the current bounds.



222
223
224
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 222

def bounds_margin_right
  page.dimensions[2] - bounds.absolute_right
end

#calc_line_metrics(line_height, font = self.font, font_size = self.font_size) ⇒ Object



403
404
405
406
407
408
409
410
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 403

def calc_line_metrics line_height, font = self.font, font_size = self.font_size
  line_height_length = line_height * font_size
  leading = line_height_length - font_size
  half_leading = leading / 2
  padding_top = half_leading + font.line_gap
  padding_bottom = half_leading
  LineMetrics.new line_height_length, leading, padding_top, padding_bottom, false
end

#catalogObject

Retrieves the catalog reference data for the PDF.



123
124
125
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 123

def catalog
  state.store.root
end

#column_box(point, options, &block) ⇒ Object

Wraps the column_box method and automatically sets the height unless the :height option is specified.



685
686
687
688
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 685

def column_box point, options, &block
  options[:height] = cursor unless options.key? :height
  super
end

#conceal_page_topObject

Prevents at_page_top? from returning true while yielding to the specified block.



256
257
258
259
260
261
262
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 256

def conceal_page_top
  old_top = (outer_bounds = ColumnBox === bounds ? bounds : @margin_box).absolute_top
  outer_bounds.instance_variable_set :@y, old_top + 0.0001
  yield
ensure
  outer_bounds.instance_variable_set :@y, old_top
end

#delete_current_pageObject

Deletes the current page and move the cursor to the previous page.



853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 853

def delete_current_page
  pg = page_number
  pdf_store = state.store
  content_id = page.content.identifier
  page_ref = page.dictionary
  (prune_dests = proc do |node|
    node.children.delete_if {|it| ::PDF::Core::NameTree::Node === it ? prune_dests[it] : it.value.data[0] == page_ref }
    false
  end)[dests.data]
  # NOTE: cannot delete objects and IDs, otherwise references get corrupted; so just reset the value
  (pdf_store.instance_variable_get :@objects)[content_id] = ::PDF::Core::Reference.new content_id, {}
  pdf_store.pages.data[:Kids].pop
  pdf_store.pages.data[:Count] -= 1
  state.pages.pop
  if pg > 1
    go_to_page pg - 1
  else
    @page_number = 0
    state.page = nil
  end
end

#dest_top(page_num = nil) ⇒ Object

Generates a destination object that resolves to the top of the page specified by the page_num parameter or the current page if no page number is provided. The destination preserves the user’s zoom level unlike the destinations generated by the outline builder.



277
278
279
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 277

def dest_top page_num = nil
  dest_xyz 0, page_height, nil, (page_num ? state.pages[page_num - 1] : page)
end

#draw_indented_formatted_line(string, options) ⇒ Object

NOTE: override built-in draw_indented_formatted_line to set first_line flag



450
451
452
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 450

def draw_indented_formatted_line string, options
  super string, (options.merge first_line: true)
end

#dry_run(keep_together: nil, pages_advanced: 0, single_page: nil, onto: nil, &block) ⇒ Object

Yields to the specified block within the context of a scratch document up to three times to acertain the extent of the content block.

The purpose of this method is two-fold. First, it works out the position where the rendered content should start in the calling document. Then, it precomputes the extent of the content starting from that position.

This method returns the content’s extent (the range from the start page and cursor to the end page and cursor) as a ScratchExtent object or, if the onto keyword parameter is specified, an Extent object. A ScratchExtent always starts the page range at 1. When the ScratchExtent is positioned onto the primary document using ScratchExtent#position_onto, that’s when the cursor may be advanced to the next page.

This method performs all work in a scratch document (or documents). It begins by starting a new page in the scratch document, first creating the scratch document if necessary. It then applies all the settings from the main document to the scratch document that impact rendering. This includes the bounds, the cursor position, and the font settings. This method assumes that the content area remains constant when content flows from one page to the next.

From this point, the number of attempts the method makes is determined by the value of the keep_together keyword parameter. If the value is true (or the parent document is inhibiting page creation), it starts from the top of the page, yields to the block, and tries to fit the content on the current page. If the content fits, it computes and returns the ScratchExtent (or Extent). If the content does not fit, it first checks if this scenario should stop the operation. If the parent document is inhibiting page creation, it bubbles the error. If the single_page keyword argument is :enforce, it raises a CannotFit error. If the single_page keyword argument is true, it returns a ScratchExtent (or Extent) that represents a full page. If none of those conditions are met, it restarts with the keep_together parameter unset.

If the keep_together parameter is not true, the method tries to render the content in the scratch document from the location of the cursor in the main document. If the cursor is at the top of the page, no special conditions are applied (this is the last attempt). The content is rendered and the extent is computed based on where the content ended up (minus a trailing empty page). If the cursor is not at the top of the page, the method renders the content while listening for a page creation event before any content is written. If a new page is created, and no content is written on the first page, the method restarts with the cursor at the top of the page.

Note that if the block has content that itself requires a dry run, that nested dry run will be performed in a separate scratch document.

The block passed to dry run should take steps to avoid leaving the document state modified. This can be done by calling ‘push_scratch doc` at the start of the block and `pop_scratch` in the ensure clause of the block.

options - A Hash of options that configure the dry run computation:

:keep_together - A Boolean indicating whether an attempt should be made to keep
the content on the same page (optional, default: false).
:single_page - A Boolean indicating whether the operation should stop if the
content exceeds the height of a single page.
:onto - The document onto which to position the scratch extent. If this option is
set, the method returns an Extent instance (optional, default: false)
:pages_advanced - The number of pages the content has been advanced during this
operation (internal only) (optional, default: 0)

Returns an Extent or ScratchExtent object that describes the bounds of the content block.



1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 1120

def dry_run keep_together: nil, pages_advanced: 0, single_page: nil, onto: nil, &block
  (scratch_pdf = scratch).start_new_page layout: page.layout, margin: page_margin
  saved_bounds = scratch_pdf.bounds
  scratch_pdf.bounds = bounds.dup.tap do |bounds_copy|
    bounds_copy.instance_variable_set :@document, scratch_pdf
    bounds_copy.instance_variable_set :@parent, saved_bounds
    bounds_copy.single_file if ColumnBox === bounds_copy
  end
  scratch_pdf.move_cursor_to cursor unless (scratch_start_at_top = keep_together || pages_advanced > 0 || at_page_top?)
  scratch_start_cursor = scratch_pdf.cursor
  scratch_start_page = scratch_pdf.page_number
  inhibit_new_page = state.on_page_create_callback == InhibitNewPageProc
  restart = nil
  scratch_pdf.font font_family, size: font_size, style: font_style do
    prev_font_scale, scratch_pdf.font_scale = scratch_pdf.font_scale, font_scale
    if keep_together || inhibit_new_page
      if (restart = scratch_pdf.perform_on_single_page { scratch_pdf.instance_exec(&block) })
        # NOTE: propogate NewPageRequiredError from nested block, which is rendered in separate scratch document
        raise NewPageRequiredError if inhibit_new_page
        if single_page
          raise ::Prawn::Errors::CannotFit if single_page == :enforce
          # single_page and onto are mutually exclusive
          return ScratchExtent.new scratch_start_page, scratch_start_cursor, scratch_start_page, 0
        end
      end
    elsif scratch_start_at_top
      scratch_pdf.instance_exec(&block)
    elsif (restart = scratch_pdf.stop_if_first_page_empty { scratch_pdf.instance_exec(&block) })
      pages_advanced += 1
    end
  ensure
    scratch_pdf.font_scale = prev_font_scale
  end
  return dry_run pages_advanced: pages_advanced, onto: onto, &block if restart
  scratch_end_page = scratch_pdf.page_number - scratch_start_page + (scratch_start_page = 1)
  if pages_advanced > 0
    scratch_start_page += pages_advanced
    scratch_end_page += pages_advanced
  end
  scratch_end_cursor = scratch_pdf.cursor
  # NOTE: drop trailing blank page and move cursor to end of previous page
  if scratch_end_page > scratch_start_page && scratch_pdf.at_page_top?
    scratch_end_page -= 1
    scratch_end_cursor = 0
  end
  extent = ScratchExtent.new scratch_start_page, scratch_start_cursor, scratch_end_page, scratch_end_cursor
  onto ? extent.position_onto(*onto) : extent
ensure
  scratch_pdf.bounds = saved_bounds
end

#effective_page_heightObject

Returns the effective (writable) height of the page

If inside a fixed-height bounding box, returns height of box.



151
152
153
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 151

def effective_page_height
  reference_bounds.height
end

#expand_grid_values(shorthand, default = nil) ⇒ Object



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 626

def expand_grid_values shorthand, default = nil
  if ::Array === shorthand
    case shorthand.size
    when 1
      [(value0 = shorthand[0] || default), value0]
    when 2
      shorthand.map {|it| it || default }
    when 4
      if Asciidoctor::PDF::ThemeLoader::CMYKColorValue === shorthand
        [shorthand, shorthand]
      else
        (shorthand.slice 0, 2).map {|it| it || default }
      end
    else
      (shorthand.slice 0, 2).map {|it| it || default }
    end
  else
    [(value0 = shorthand || default), value0]
  end
end

#expand_indent_value(value) ⇒ Object



598
599
600
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 598

def expand_indent_value value
  (::Array === value ? (value.slice 0, 2) : (::Array.new 2, value)).map(&:to_f)
end

#expand_padding_value(shorthand) ⇒ Object Also known as: expand_margin_value



602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 602

def expand_padding_value shorthand
  (@edge_shorthand_cache ||= ::Hash.new do |store, key|
    if ::Array === key
      case key.size
      when 1
        value = [(value0 = key[0] || 0), value0, value0, value0]
      when 2
        value = [(value0 = key[0] || 0), (value1 = key[1] || 0), value0, value1]
      when 3
        value = [key[0] || 0, (value1 = key[1] || 0), key[2] || 0, value1]
      when 4
        value = key.map {|it| it || 0 }
      else
        value = (key.slice 0, 4).map {|it| it || 0 }
      end
    else
      value = [(value0 = key || 0), value0, value0, value0]
    end
    store[key] = value
  end)[shorthand]
end

#expand_rect_values(shorthand, default = nil) ⇒ Object



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 647

def expand_rect_values shorthand, default = nil
  if ::Array === shorthand
    case shorthand.size
    when 1
      [(value0 = shorthand[0] || default), value0, value0, value0]
    when 2
      [(value0 = shorthand[0] || default), (value1 = shorthand[1] || default), value0, value1]
    when 3
      [shorthand[0] || default, (value1 = shorthand[1] || default), shorthand[2] || default, value1]
    when 4
      if Asciidoctor::PDF::ThemeLoader::CMYKColorValue === shorthand
        [shorthand, shorthand, shorthand, shorthand]
      else
        shorthand.map {|it| it || default }
      end
    else
      (shorthand.slice 0, 4).map {|it| it || default }
    end
  else
    [(value0 = shorthand || default), value0, value0, value0]
  end
end

#fill_absolute_bounds(f_color) ⇒ Object

Fills the absolute bounding box with the specified fill color. Before returning from this method, the original fill color on the document is restored.



720
721
722
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 720

def fill_absolute_bounds f_color
  canvas { fill_bounds f_color }
end

#fill_and_stroke_bounds(f_color = fill_color, s_color = stroke_color, options = {}) ⇒ Object

Fills the current bounds using the specified fill color and strokes the bounds using the specified stroke color. Sets the line with if specified in the options. Before returning from this method, the original fill color, stroke color and line width on the document are restored.



729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 729

def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
  no_fill = !f_color || f_color == 'transparent'
  if ::Array === (s_width = options[:line_width] || 0)
    s_width = [s_width[0], s_width[1], s_width[0], s_width[1]] if s_width.size == 2
    s_width_max = (s_width = s_width.map {|it| it || 0 }).max
    radius = 0
  else
    radius = options[:radius] || 0
  end
  no_stroke = !s_color || s_color == 'transparent' || (s_width_max || s_width) == 0
  return if no_fill && no_stroke
  save_graphics_state do
    # fill
    unless no_fill
      fill_color f_color
      fill_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
    end

    next if no_stroke

    # stroke
    if s_width_max
      s_width_top, s_width_right, s_width_bottom, s_width_left = s_width
      projection_top, projection_right, projection_bottom, projection_left = s_width.map {|it| it * 0.5 }
      if s_width_top > 0
        stroke_horizontal_rule s_color, line_width: s_width_top, line_style: options[:line_style], left_projection: projection_left, right_projection: projection_right
      end
      if s_width_right > 0
        stroke_vertical_rule s_color, line_width: s_width_right, line_style: options[:line_style], at: bounds.width, top_projection: projection_top, bottom_projection: projection_bottom
      end
      if s_width_bottom > 0
        stroke_horizontal_rule s_color, line_width: s_width_bottom, line_style: options[:line_style], at: bounds.height, left_projection: projection_left, right_projection: projection_right
      end
      if s_width_left > 0
        stroke_vertical_rule s_color, line_width: s_width_left, line_style: options[:line_style], top_projection: projection_top, bottom_projection: projection_bottom
      end
    else
      stroke_color s_color
      case options[:line_style]
      when :dashed
        line_width s_width
        dash s_width * 4
      when :dotted
        line_width s_width
        dash s_width
      when :double
        single_line_width = s_width / 3.0
        line_width single_line_width
        inner_line_offset = single_line_width * 2
        inner_top_left = [bounds.left + inner_line_offset, bounds.top - inner_line_offset]
        stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
        stroke_rounded_rectangle inner_top_left, bounds.width - (inner_line_offset * 2), bounds.height - (inner_line_offset * 2), radius
        next
      else # :solid
        line_width s_width
      end
      stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
    end
  end
end

#fill_bounds(f_color) ⇒ Object

Fills the current bounding box with the specified fill color. Before returning from this method, the original fill color on the document is restored.



710
711
712
713
714
715
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 710

def fill_bounds f_color
  prev_fill_color = fill_color
  fill_color f_color
  fill_rectangle bounds.top_left, bounds.width, bounds.height
  fill_color prev_fill_color
end

#fill_formatted_text_box(text, options) ⇒ Object

NOTE: override built-in fill_formatted_text_box to insert leading before second line when :first_line is true



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 425

def fill_formatted_text_box text, options
  if (initial_gap = options[:initial_gap]) && !text.empty? && text[0][:from_page] != page_number
    self.y -= initial_gap
  end
  merge_text_box_positioning_options options
  box = ::Prawn::Text::Formatted::Box.new text, options
  remaining_fragments = box.render
  @no_text_printed = box.nothing_printed?
  @all_text_printed = box.everything_printed?
  unless remaining_fragments.empty? || (remaining_fragments[0][:from_page] ||= page_number) == page_number
    log :error, %(cannot fit formatted text on page: #{remaining_fragments.map {|it| it[:image_path] || it[:text] }.join})
    page.tare_content_stream
    remaining_fragments = {}
  end

  if @final_gap || (options[:first_line] && !(@no_text_printed || @all_text_printed))
    self.y -= box.height + box.line_gap + box.leading
  else
    self.y -= box.height
  end

  remaining_fragments
end

#floatObject

Override the built-in float method to add support for restoring the current column of a ColumnBox



520
521
522
523
524
525
526
527
528
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 520

def float
  original_page_number = page_number
  original_y = y
  original_column = bounds.current_column if ColumnBox === bounds
  yield
  go_to_page original_page_number unless page_number == original_page_number
  self.y = original_y
  bounds.current_column = original_column if original_column
end

#flow_bounding_box(options = {}) ⇒ Object

A flowing version of bounding_box. If the content runs to another page, the cursor starts at the top of the page instead of from the original cursor position. Similar to span, except the :position option is limited to a numeric value and additional options are passed through to bounding_box.



695
696
697
698
699
700
701
702
703
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 695

def flow_bounding_box options = {}
  original_y, original_x = y, bounds.absolute_left
  canvas do
    bounding_box [original_x + (options.delete :position).to_f, @margin_box.absolute_top], options do
      self.y = original_y
      yield
    end
  end
end

#font(name = nil, options = {}) ⇒ Object

Enhances the built-in font method to allow the font size to be specified as the second option and to lazily load font-based icons.



317
318
319
320
321
322
323
324
325
326
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 317

def font name = nil, options = {}
  if name
    options = { size: options } if ::Numeric === options
    if IconSets.include? name
      icon_font_data name
      options = options.reject {|k| k == :style } if options.key? :style
    end
  end
  super
end

#font_familyObject Also known as: font_name

Retrieves the current font name (i.e., family).



330
331
332
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 330

def font_family
  font.options[:family]
end

#font_infoObject

Retrieves the current font info (family, style, size) as a Hash



338
339
340
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 338

def font_info
  { family: font.options[:family], style: (font.options[:style] || :normal), size: @font_size }
end

#font_size(points = nil) ⇒ Object

Applies points as a scale factor of the current font if the value provided is less than or equal to 1 or it’s a string (e.g., 1.1em), then delegates to the super implementation to carry out the built-in functionality.

– QUESTION: should we round the result?



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 358

def font_size points = nil
  return @font_size unless points
  if ::String === points
    if points.end_with? 'rem'
      super @root_font_size * points.to_f
    elsif points.end_with? 'em'
      super @font_size * points.to_f
    elsif points.end_with? '%'
      super @font_size * (points.to_f / 100)
    else
      super points.to_f
    end
  else
    super points
  end
end

#font_style(style = nil) ⇒ Object

Set the font style on the document, if a style is given, otherwise return the current font style.



344
345
346
347
348
349
350
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 344

def font_style style = nil
  if style
    font font.options[:family], style: style
  else
    font.options[:style] || :normal
  end
end

#font_styles(style = font_style) ⇒ Object

Retreives the collection of font styles from the given font style key, which defaults to the current font style.



393
394
395
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 393

def font_styles style = font_style
  FontStyleToSet[style].dup
end

#generate_margin_boxObject

remove once fixed upstream; see github.com/prawnpdf/prawn/pull/1122



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 156

def generate_margin_box
  page_w, page_h = (page = state.page).dimensions.slice 2, 2
  page_m = page.margins
  prev_margin_box, @margin_box = @margin_box, (::Prawn::Document::BoundingBox.new self, nil, [page_m[:left], page_h - page_m[:top]], width: page_w - page_m[:left] - page_m[:right], height: page_h - page_m[:top] - page_m[:bottom])

  # update bounding box if not flowing from the previous page
  unless @bounding_box&.parent
    prev_margin_box = @bounding_box
    @bounding_box = @margin_box
  end

  # maintains indentation settings across page breaks
  if prev_margin_box
    @margin_box.add_left_padding prev_margin_box.total_left_padding
    @margin_box.add_right_padding prev_margin_box.total_right_padding
  end

  nil
end

#get_dest(name, node = dests.data) ⇒ Object

Gets the destination registered for the specified name. The return value matches that which was passed to the add_dest method.



284
285
286
287
288
289
290
291
292
293
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 284

def get_dest name, node = dests.data
  node.children.each do |child|
    if ::PDF::Core::NameTree::Value === child
      return child.value.data if child.name == name
    elsif (found = get_dest name, child)
      return found
    end
  end
  nil
end

#hyphenate_text(text, hyphenator) ⇒ Object



512
513
514
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 512

def hyphenate_text text, hyphenator
  hyphenate_words_pcdata text, hyphenator
end

#image_page(file, options = {}) ⇒ Object

Create a new page for the specified image.

The image is positioned relative to the boundaries of the page.



917
918
919
920
921
922
923
924
925
926
927
928
929
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 917

def image_page file, options = {}
  start_new_page_discretely
  ex = nil
  float do
    canvas do
      image file, ({ position: :center, vposition: :center }.merge options)
    rescue
      ex = $!
    end
  end
  raise ex if ex
  nil
end

#import_page(file, options = {}) ⇒ Object

Import the specified page into the current document.

By default, advance to the next page afterwards, creating it if necessary. This behavior can be disabled by passing the option ‘advance: false`. However, due to how page creation works in Prawn, understand that advancing to the next page is necessary to prevent the size & layout of the imported page from affecting a newly created page.



882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 882

def import_page file, options = {}
  prev_page_layout = page.layout
  prev_page_size = page.size
  prev_page_margin = page_margin
  prev_bounds = bounds
  state.compress = false if state.compress # can't use compression if using template
  prev_text_rendering_mode = (defined? @text_rendering_mode) ? @text_rendering_mode : nil
  delete_current_page if options[:replace]
  # NOTE: use functionality provided by prawn-templates
  start_new_page_discretely template: file, template_page: options[:page]
  # prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
  @text_rendering_mode = prev_text_rendering_mode
  if page.imported_page?
    yield if block_given?
    # NOTE: set page size & layout explicitly in case imported page differs
    # I'm not sure it's right to start a new page here, but unfortunately there's no other
    # way atm to prevent the size & layout of the imported page from affecting subsequent pages
    if options.fetch :advance, true
      advance_page layout: prev_page_layout, margin: prev_page_margin, size: prev_page_size
      (@bounding_box = prev_bounds).reset_top if ColumnBox === prev_bounds
    end
  elsif options.fetch :advance_if_missing, true
    delete_current_page
    # NOTE: see previous comment
    advance_page layout: prev_page_layout, margin: prev_page_margin, size: prev_page_size
    @y = (@bounding_box = prev_bounds).reset_top if ColumnBox === prev_bounds
  else
    delete_current_page
  end
  nil
end

#last_page?Boolean

Returns whether the current page is the last page in the document.

Returns:

  • (Boolean)


266
267
268
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 266

def last_page?
  page_number == page_count
end

#min_versionObject

Retrieves the compatiblity version of the PDF.



129
130
131
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 129

def min_version
  state.version
end

#move_down(n) ⇒ Object

Short-circuits the call to the built-in move_down operation when n is 0.



546
547
548
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 546

def move_down n
  super unless n == 0
end

#move_text_position(h) ⇒ Object

Override built-in move_text_position method to prevent Prawn from advancing to next page if image doesn’t fit before rendering image. – NOTE: could use :at option when calling image/embed_image instead



541
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 541

def move_text_position h; end

#move_up(n) ⇒ Object

Short-circuits the call to the built-in move_up operation when n is 0.



533
534
535
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 533

def move_up n
  super unless n == 0
end

#pad_box(padding, node = nil) ⇒ Object

Augments the built-in pad method by adding support for specifying padding on all four sizes.

Padding may be specified as an array of four values, or as a single value. The single value is used as the padding around all four sides of the box.

If padding is nil, this method simply yields to the block and returns.

Example:

pad_box 20 do
  text 'A paragraph inside a blox with even padding from all edges.'
end

pad_box [10, 5] do
  text 'A paragraph inside a box with different padding from ends and sides.'
end

pad_box [5, 10, 15, 20] do
  text 'A paragraph inside a box with different padding from each edge.'
end


573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 573

def pad_box padding, node = nil
  if padding
    p_top, p_right, p_bottom, p_left = expand_padding_value padding
    # logic is intentionally inlined
    begin
      if node && ((last_block = node).content_model != :compound || (last_block = node.last_child)&.context == :paragraph)
        @bottom_gutters << { last_block => p_bottom }
      else
        @bottom_gutters << {}
      end
      move_down p_top
      bounds.add_left_padding p_left
      bounds.add_right_padding p_right
      yield
    ensure
      cursor > p_bottom ? (move_down p_bottom) : bounds.move_past_bottom unless at_page_top?
      @bottom_gutters.pop
      bounds.subtract_left_padding p_left
      bounds.subtract_right_padding p_right
    end
  else
    yield
  end
end

#page_heightObject

Returns the height of the current page from edge-to-edge



143
144
145
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 143

def page_height
  page.dimensions[3]
end

#page_marginObject

Returns the margins for the current page as a 4 element array (top, right, bottom, left)



186
187
188
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 186

def page_margin
  [page_margin_top, page_margin_right, page_margin_bottom, page_margin_left]
end

#page_margin_bottomObject

Returns the width of the bottom margin for the current page



210
211
212
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 210

def page_margin_bottom
  page.margins[:bottom]
end

#page_margin_leftObject

Returns the width of the left margin for the current page



192
193
194
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 192

def page_margin_left
  page.margins[:left]
end

#page_margin_rightObject

Returns the width of the right margin for the current page



198
199
200
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 198

def page_margin_right
  page.margins[:right]
end

#page_margin_topObject

Returns the width of the top margin for the current page



204
205
206
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 204

def page_margin_top
  page.margins[:top]
end

#page_side(pgnum = nil, invert = nil) ⇒ Object

Returns the side the current page is facing, :recto or :verso.



228
229
230
231
232
233
234
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 228

def page_side pgnum = nil, invert = nil
  if invert
    (recto_page? pgnum) ? :verso : :recto
  else
    (verso_page? pgnum) ? :verso : :recto
  end
end

#page_widthObject

Returns the width of the current page from edge-to-edge



137
138
139
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 137

def page_width
  page.dimensions[2]
end

#parse_text(string, options = {}) ⇒ Object

Parse the text into an array of fragments using the text formatter.



413
414
415
416
417
418
419
420
421
422
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 413

def parse_text string, options = {}
  return [] if string.nil?

  if (format_option = options[:inline_format])
    format_option = [] unless ::Array === format_option
    text_formatter.format string, *format_option
  else
    [text: string]
  end
end

#perform_discretelyObject

Perform an operation (such as creating a new page) without triggering the on_page_create callback



933
934
935
936
937
938
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 933

def perform_discretely
  state.on_page_create_callback = nil if (saved_callback = state.on_page_create_callback) != InhibitNewPageProc
  yield
ensure
  state.on_page_create_callback = saved_callback
end

#perform_on_single_pageObject

This method installs an on_page_create_callback that stops processing if the first page is exceeded while yielding to the specified block. If the content fits on a single page, the processing is not stopped. The purpose of this method is to determine if the content fits on a single page.

Returns a Boolean indicating whether the content fits on a single page.



1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 1011

def perform_on_single_page
  saved_callback, state.on_page_create_callback = state.on_page_create_callback, InhibitNewPageProc
  yield
  false
rescue NewPageRequiredError
  true
ensure
  state.on_page_create_callback = saved_callback
end

#recto_page?(pgnum = nil) ⇒ Boolean

Returns whether the page is a recto page.

Returns:

  • (Boolean)


238
239
240
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 238

def recto_page? pgnum = nil
  (pgnum || page_number).odd?
end

#register_font(data) ⇒ Object

Registers a new custom font described in the data parameter after converting the font name to a String.

Example:

register_font Roboto: {
  normal: 'fonts/roboto-normal.ttf',
  italic: 'fonts/roboto-italic.ttf',
  bold: 'fonts/roboto-bold.ttf',
  bold_italic: 'fonts/roboto-bold_italic.ttf'
}


309
310
311
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 309

def register_font data
  font_families.update data.transform_keys(&:to_s)
end

#resolve_font_style(styles) ⇒ Object



380
381
382
383
384
385
386
387
388
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 380

def resolve_font_style styles
  if styles.include? :bold
    (styles.include? :italic) ? :bold_italic : :bold
  elsif styles.include? :italic
    :italic
  else
    :normal
  end
end

#scratchObject



959
960
961
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 959

def scratch
  @scratch ||= ((Marshal.load Marshal.dump @scratch_prototype).send :init_scratch, self)
end

#scratch?Boolean

Returns:

  • (Boolean)


963
964
965
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 963

def scratch?
  @label == :scratch
end

#set_font(font, size = nil) ⇒ Object



375
376
377
378
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 375

def set_font font, size = nil
  @font = font
  font_size size if size
end

#set_page_margin(margin) ⇒ Object

Set the margins for the current page.



178
179
180
181
182
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 178

def set_page_margin margin
  # FIXME: is there a cleaner way to set margins? does it make sense to override create_new_page?
  apply_margin_options margin: margin
  generate_margin_box
end

#span_page_width_if(verdict) ⇒ Object

Stretch the current bounds to the left and right edges of the current page while yielding the specified block if the verdict argument is true. Otherwise, simply yield the specified block.



674
675
676
677
678
679
680
681
682
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 674

def span_page_width_if verdict
  if verdict
    indent(-bounds_margin_left, -bounds_margin_right) do
      yield
    end
  else
    yield
  end
end

#start_new_page_discretely(options = {}) ⇒ Object

Start a new page without triggering the on_page_create callback



949
950
951
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 949

def start_new_page_discretely options = {}
  perform_discretely { start_new_page options }
end

#stop_if_first_page_emptyObject

This method installs an on_page_create_callback that stops processing if a new page is created without writing content to the first page while yielding to the specified block. If any content is written to the first page, processing is not stopped. The purpose of this method is to check whether any content fits on the remaining space on the current page.

Returns a Boolean indicating whether any content is written on the first page.



1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 1027

def stop_if_first_page_empty
  delegate = state.on_page_create_callback
  state.on_page_create_callback = DetectEmptyFirstPageProc.curry[delegate].extend DetectEmptyFirstPage
  yield
  false
rescue NewPageRequiredError
  true
ensure
  state.on_page_create_callback = delegate
end

#stroke_horizontal_rule(rule_color = stroke_color, options = {}) ⇒ Object

Strokes a horizontal line using the current bounds. The width of the line can be specified using the line_width option. The offset from the cursor can be set using the at option.



794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 794

def stroke_horizontal_rule rule_color = stroke_color, options = {}
  rule_y = cursor - (options[:at] || 0)
  rule_style = options[:line_style]
  rule_width = options[:line_width] || 0.5
  rule_x_start = bounds.left - (options[:left_projection] || 0)
  rule_x_end = bounds.right - (options[:right_projection] || 0)
  save_graphics_state do
    stroke_color rule_color
    case rule_style
    when :dashed
      line_width rule_width
      dash rule_width * 4
    when :dotted
      line_width rule_width
      dash rule_width
    when :double
      single_rule_width = rule_width / 3.0
      line_width single_rule_width
      stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y + single_rule_width)
      stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y - single_rule_width)
      next
    else # :solid
      line_width rule_width
    end
    stroke_horizontal_line rule_x_start, rule_x_end, at: rule_y
  end
end

#stroke_vertical_rule(rule_color = stroke_color, options = {}) ⇒ Object

A compliment to the stroke_horizontal_rule method, strokes a vertical line using the current bounds. The width of the line can be specified using the line_width option. The horizontal (x) position can be specified using the at option.



827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 827

def stroke_vertical_rule rule_color = stroke_color, options = {}
  rule_x = options[:at] || 0
  rule_y_from = bounds.top + (options[:top_projection] || 0)
  rule_y_to = bounds.bottom - (options[:bottom_projection] || 0)
  rule_style = options[:line_style]
  rule_width = options[:line_width] || 0.5
  save_graphics_state do
    line_width rule_width
    stroke_color rule_color
    case rule_style
    when :dashed
      dash rule_width * 4
    when :dotted
      dash rule_width
    when :double
      stroke_vertical_line rule_y_from, rule_y_to, at: (rule_x - rule_width)
      rule_x += rule_width
    end if rule_style
    stroke_vertical_line rule_y_from, rule_y_to, at: rule_x
  end
end

#tare_first_page_content_streamObject

This method delegates to the provided block, then tares (i.e., resets) the content stream of the initial page.

The purpose of this method is to ink content while making it appear as though the page is empty. This technique allows the caller to detect whether any subsequent content was written to the page following the content inked by the block. It’s often used to keep the title of a content block with the block’s first child.

NOTE: this method should only used inside dry_run since that’s when DetectEmptyFirstPage is active



1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 1047

def tare_first_page_content_stream
  return yield unless DetectEmptyFirstPage === (delegate = state.on_page_create_callback)
  on_page_create_called = nil
  state.on_page_create_callback = proc do |pdf|
    on_page_create_called = true
    pdf.state.pages[pdf.page_number - 2].tare_content_stream
    delegate.call pdf
  end
  begin
    yield
  ensure
    page.tare_content_stream unless on_page_create_called
    state.on_page_create_callback = delegate
  end
end

#text_with_formatted_first_line(string, first_line_options, options) ⇒ Object

Performs the same work as Prawn::Text.text except that the first_line_options are applied to the first line of text renderered. It’s necessary to use low-level APIs in this method so we only style the first line and not the remaining lines (which is the default behavior in Prawn).



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 457

def text_with_formatted_first_line string, first_line_options, options
  if (first_line_font_color = first_line_options.delete :color)
    remaining_lines_font_color, options[:color] = options[:color], first_line_font_color
  end
  fragments = parse_text string, options
  # NOTE: the low-level APIs we're using don't recognize the :styles option, so we must resolve
  # NOTE: disabled until we have a need for it; currently handled in convert_abstract
  #if (styles = options.delete :styles)
  #  options[:style] = resolve_font_style styles
  #end
  if (first_line_styles = first_line_options.delete :styles)
    first_line_options[:style] = resolve_font_style first_line_styles
  end
  first_line_text_transform = first_line_options.delete :text_transform
  options = options.merge document: self
  @final_gap = final_gap = options.delete :final_gap
  text_indent = options.delete :indent_paragraphs
  # QUESTION: should we merge more carefully here? (hand-select keys?)
  first_line_options = (options.merge first_line_options).merge single_line: true, first_line: true
  box = ::Prawn::Text::Formatted::Box.new fragments, first_line_options
  if text_indent
    remaining_fragments = indent(text_indent) { box.render dry_run: true }
  else
    remaining_fragments = box.render dry_run: true
  end
  if remaining_fragments.empty?
    remaining_fragments = nil
  elsif (remaining_fragments[0][:from_page] ||= page_number) != page_number
    log :error, %(cannot fit formatted text on page: #{remaining_fragments.map {|it| it[:image_path] || it[:text] }.join})
    page.tare_content_stream
    remaining_fragments = nil
  end
  if first_line_text_transform
    # NOTE: applying text transform here could alter the wrapping, so isolate first line and shrink it to fit
    first_line_fragments = (box.instance_variable_get :@arranger).consumed
    fragments = first_line_fragments.map {|fragment| fragment.merge text: (transform_text fragment[:text], first_line_text_transform) }
    first_line_options[:overflow] = :shrink_to_fit
    if remaining_fragments
      @final_gap = true
      first_line_options[:force_justify] = true if first_line_options[:align] == :justify && first_line_fragments[-1][:text] != ?\n
    end
  end
  if text_indent
    indent(text_indent) { fill_formatted_text_box fragments, first_line_options }
  else
    fill_formatted_text_box fragments, first_line_options
  end
  if remaining_fragments
    options[:color] = remaining_lines_font_color if first_line_font_color
    @final_gap = final_gap if first_line_text_transform
    remaining_fragments = fill_formatted_text_box remaining_fragments, options
    draw_remaining_formatted_text_on_new_pages remaining_fragments, options
  end
end

#verso_page?(pgnum = nil) ⇒ Boolean

Returns whether the page is a verso page.

Returns:

  • (Boolean)


244
245
246
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 244

def verso_page? pgnum = nil
  (pgnum || page_number).even?
end

#width_of_string(string, options) ⇒ Object

Override width of string to check for placeholder char, which uses character spacing to control width



399
400
401
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 399

def width_of_string string, options
  string == PlaceholderChar ? @character_spacing : super
end

#with_dry_run {|dry_run(&block).position_onto self, cursor| ... } ⇒ Object

Yields:

  • (dry_run(&block).position_onto self, cursor)


967
968
969
# File 'lib/asciidoctor/pdf/ext/prawn/extensions.rb', line 967

def with_dry_run &block
  yield dry_run(&block).position_onto self, cursor
end