class Asciidoctor::PDF::Converter

Constants

AdmonitionIcons
AlignmentTable
AsciidoctorVersion
BallotBox

NOTE Default theme font uses ballot boxes from FontAwesome

BlankLineRx
BlockAlignmentNames
Bullets
CalloutExtractRx
CodeRayRequirePath

NOTE require_library doesn't support require_relative and we don't modify the load path for this gem

ColumnPositions
DotLeaderTextDefault
DoubleLF
DummyText
EmDash
GuardedIndent

a no-break space is used to replace a leading space to prevent Prawn from trimming indentation a leading zero-width space can't be used as it gets dropped when calculating the line width

GuardedInnerIndent
ImageAttributeValueRx
InnerIndent
LF
LineScanRx
LowercaseGreekA
MeasurementPartsRx
MeasurementRxt
NarrowNoBreakSpace
NoBreakSpace
PageLayouts
PageSides
PageSizeRx
PygmentsBgColorRx
RightPointer
RougeRequirePath
SimpleAttributeRefRx
SourceHighlighters
TAB
TabIndentRx
TabRx
TextAlignmentNames
TextAlignmentRoles
UriBreakCharRepl
UriBreakCharsRx
UriSchemeBoundaryRx
ViewportWidth
WhitespaceChars
ZeroWidthSpace

Public Class Methods

new(backend, opts) click to toggle source
Calls superclass method
# File lib/asciidoctor-pdf/converter.rb, line 105
def initialize backend, opts
  super
  basebackend 'html'
  filetype 'pdf'
  htmlsyntax 'html'
  outfilesuffix '.pdf'
  if (doc = opts[:document])
    # NOTE enabling data-uri forces Asciidoctor Diagram to produce absolute image paths
    doc.attributes['data-uri'] = ((doc.instance_variable_get :@attribute_overrides) || {})['data-uri'] = ''
  end
  @capabilities = {
    expands_tabs: (::Asciidoctor::VERSION.start_with? '1.5.3.') || AsciidoctorVersion >= (::Gem::Version.create '1.5.3'),
    special_sectnums: AsciidoctorVersion >= (::Gem::Version.create '1.5.7'),
    syntax_highlighter: AsciidoctorVersion >= (::Gem::Version.create '2.0.0'),
  }
end

Public Instance Methods

add_dest_for_block(node, id = nil) click to toggle source

If an id is provided or the node passed as the first argument has an id, add a named destination to the document equivalent to the node id at the current y position. If the node does not have an id and an id is not specified, do nothing.

If the node is a section, and the current y position is the top of the page, set the y position equal to the page height to improve the navigation experience. If the current x position is at or inside the left margin, set the x position equal to 0 (left edge of page) to improve the navigation experience.

# File lib/asciidoctor-pdf/converter.rb, line 3442
def add_dest_for_block node, id = nil
  if !scratch? && (id ||= node.id)
    dest_x = bounds.absolute_left.truncate 4
    # QUESTION when content is aligned to left margin, should we keep precise x value or just use 0?
    dest_x = 0 if dest_x <= page_margin_left
    dest_y = at_page_top? && (node.context == :section || node.context == :document) ? page_height : y
    # TODO find a way to store only the ref of the destination; look it up when we need it
    node.set_attr 'pdf-destination', (node_dest = (dest_xyz dest_x, dest_y))
    add_dest id, node_dest
  end
  nil
end
add_outline(doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3086
def add_outline doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0
  front_matter_counter = RomanNumeral.new 0, :lower
  pagenum_labels = {}

  num_front_matter_pages.times do |n|
    pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new front_matter_counter.next!.to_s) }
  end

  # add labels for each content page, which is required for reader's page navigator to work correctly
  (num_front_matter_pages..(page_count - 1)).each_with_index do |n, i|
    pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new %(#{i + 1})) }
  end

  outline.define do
    # FIXME use sanitize: :plain_text once available
    if (doctitle = document.sanitize(doc.doctitle use_fallback: true))
      # FIXME link to title page if there's a cover page (skip cover page and ensuing blank page)
      page title: doctitle, destination: (document.dest_top 1)
    end
    page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) unless toc_page_nums.none?
    # QUESTION any way to get add_outline_level to invoke in the context of the outline?
    document.add_outline_level self, doc.sections, num_levels
  end

  catalog.data[:PageLabels] = state.store.ref Nums: pagenum_labels.flatten
  catalog.data[:PageMode] = :UseOutlines
  nil
end
add_outline_level(outline, sections, num_levels) click to toggle source

FIXME only nest inside root node if doctype=article

# File lib/asciidoctor-pdf/converter.rb, line 3116
def add_outline_level outline, sections, num_levels
  sections.each do |sect|
    sect_title = sanitize sect.numbered_title formal: true
    sect_destination = sect.attr 'pdf-destination'
    if (level = sect.level) == num_levels || !sect.sections?
      outline.page title: sect_title, destination: sect_destination
    elsif level <= num_levels
      outline.section sect_title, { destination: sect_destination } do
        add_outline_level outline, sect.sections, num_levels
      end
    end
  end
end
admonition_icon_data(key) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2709
def admonition_icon_data key
  if (icon_data = @theme[%(admonition_icon_#{key})])
    (AdmonitionIcons[key] || {}).merge icon_data
  else
    AdmonitionIcons[key]
  end
end
arrange_fragments_by_line(fragments, opts = {}) click to toggle source

Arrange fragments by line in an arranger and return an unfinalized arranger.

Finalizing the arranger is deferred since it must be done in the context of the global font settings you want applied to each fragment.

# File lib/asciidoctor-pdf/converter.rb, line 3280
def arrange_fragments_by_line fragments, opts = {}
  arranger = ::Prawn::Text::Formatted::Arranger.new self
  by_line = arranger.consumed = []
  fragments.each do |fragment|
    if (txt = fragment[:text]) == LF
      by_line << fragment
    elsif txt.include? LF
      txt.scan(LineScanRx) do |line|
        by_line << (line == LF ? { text: LF } : (fragment.merge text: line))
      end
    else
      by_line << fragment
    end
  end
  arranger
end
breakable_uri(uri) click to toggle source

NOTE assume URL is escaped (i.e., contains character references such as &amp;)

# File lib/asciidoctor-pdf/converter.rb, line 3627
def breakable_uri uri
  scheme, address = uri.split UriSchemeBoundaryRx, 2
  address, scheme = scheme, address unless address
  unless address.nil_or_empty?
    address = address.gsub UriBreakCharsRx, UriBreakCharRepl
    # NOTE require at least two characters after a break
    address.slice!(-2) if address[-2] == ZeroWidthSpace
  end
  %(#{scheme}#{address})
end
build_pdf_info(doc) click to toggle source

FIXME Pdfmark should use the PDF info result

# File lib/asciidoctor-pdf/converter.rb, line 433
def build_pdf_info doc
  info = {}
  # FIXME use sanitize: :plain_text once available
  info[:Title] = sanitize(doc.doctitle use_fallback: true).as_pdf
  if doc.attr? 'authors'
    info[:Author] = (doc.attr 'authors').as_pdf
  end
  if doc.attr? 'subject'
    info[:Subject] = (doc.attr 'subject').as_pdf
  end
  if doc.attr? 'keywords'
    info[:Keywords] = (doc.attr 'keywords').as_pdf
  end
  if (doc.attr? 'publisher')
    info[:Producer] = (doc.attr 'publisher').as_pdf
  end
  info[:Creator] = %(Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION}).as_pdf
  info[:Producer] ||= (info[:Author] || info[:Creator])
  unless doc.attr? 'reproducible'
    # NOTE since we don't track the creation date of the input file, we map the ModDate header to the last modified
    # date of the input document and the CreationDate header to the date the PDF was produced by the converter.
    info[:ModDate] = ::Time.parse(doc.attr 'docdatetime') rescue (now ||= ::Time.now)
    info[:CreationDate] = ::Time.parse(doc.attr 'localdatetime') rescue (now ||= ::Time.now)
  end
  info
end
build_pdf_options(doc, theme) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 351
def build_pdf_options doc, theme
  case (page_margin = (doc.attr 'pdf-page-margin') || theme.page_margin)
  when ::Array
    page_margin = page_margin[0..3] if page_margin.length > 4
    page_margin = page_margin.map {|v| ::Numeric === v ? v : (str_to_pt v.to_s) }
  when ::Numeric
    page_margin = [page_margin]
  when ::String
    if page_margin.empty?
      page_margin = nil
    elsif (page_margin.start_with? '[') && (page_margin.end_with? ']')
      if (page_margin = page_margin[1...-1].rstrip).empty?
        page_margin = [0]
      else
        if (page_margin = page_margin.split ',', -1).length > 4
          page_margin = page_margin[0..3]
        end
        page_margin = page_margin.map {|v| str_to_pt v.rstrip }
      end
    else
      page_margin = [(str_to_pt page_margin)]
    end
  else
    page_margin = nil
  end

  if (doc.attr? 'pdf-page-size') && PageSizeRx =~ (doc.attr 'pdf-page-size')
    # e.g, [8.5in, 11in]
    if $1
      page_size = [$1, $2]
    # e.g, 8.5in x 11in
    elsif $3
      page_size = [$3, $4]
    # e.g, A4
    else
      page_size = $&
    end
  else
    page_size = theme.page_size
  end

  page_size = case page_size
  when ::String
    # TODO extract helper method to check for named page size
    if ::PDF::Core::PageGeometry::SIZES.key?(page_size = page_size.upcase)
      page_size
    end
  when ::Array
    unless page_size.size == 2
      page_size = page_size[0..1].fill(0..1) {|i| page_size[i] || 0}
    end
    page_size.map do |dim|
      if ::Numeric === dim
        # dimension cannot be less than 0
        dim > 0 ? dim : break
      elsif ::String === dim && MeasurementPartsRx =~ dim
        # NOTE truncate to max precision retained by PDF::Core
        (to_pt $1.to_f, $2).truncate 4
      else
        break
      end
    end
  end

  if (page_layout = (doc.attr 'pdf-page-layout') || theme.page_layout).nil_or_empty? ||
      !(PageLayouts.include?(page_layout = page_layout.to_sym))
    page_layout = nil
  end

  {
    #compress: true,
    #optimize_objects: true,
    margin: (page_margin || 36),
    page_size: (page_size || 'A4'),
    page_layout: (page_layout || :portrait),
    info: (build_pdf_info doc),
    skip_page_creation: true,
    text_formatter: (FormattedText::Formatter.new theme: theme)
  }
end
consolidate_ranges(nums) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3638
def consolidate_ranges nums
  if nums.size > 1
    prev = nil
    nums.inject([]) {|accum, num|
      if prev && (prev.to_i + 1) == num.to_i
        accum[-1][1] = num
      else
        accum << [num]
      end
      prev = num
      accum
    }.map {|range| range.join '-' }
  else
    nums
  end
end
conum_glyph(number) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1680
def conum_glyph number
  # FIXME make starting glyph a constant and/or theme setting
  # FIXME use lookup table for glyphs instead of relying on counting
  # \u2460 = circled one, \u24f5 = double circled one, \u278b = negative circled one
  glyph = %(\u2460)
  (number - 1).times { glyph = glyph.next }
  glyph
end
convert(node, name = nil, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 122
def convert node, name = nil, opts = {}
  method_name = %(convert_#{name ||= node.node_name})
  result = nil
  if respond_to? method_name
    # NOTE we prepend the prefix "convert_" to avoid conflict with Prawn methods
    result = send method_name, node
  else
    # TODO delegate to convert_method_missing
    logger.warn %(conversion missing in backend #{@backend} for #{name})
  end
  # NOTE inline nodes generate pseudo-HTML strings; the remainder write directly to PDF object
  ::Asciidoctor::Inline === node ? result : self
end
convert_abstract(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 527
def convert_abstract node
  add_dest_for_block node if node.id
  pad_box @theme.abstract_padding do
    if node.title?
      theme_font :abstract_title do
        layout_heading node.title, align: (@theme.abstract_title_align || @base_align).to_sym
      end
    end
    theme_font :abstract do
      prose_opts = { line_height: @theme.abstract_line_height, align: (@theme.abstract_align || @base_align).to_sym }
      if (text_indent = @theme.prose_text_indent)
        prose_opts[:indent_paragraphs] = text_indent
      end
      # FIXME control more first_line_options using theme
      if (line1_font_style = @theme.abstract_first_line_font_style) && line1_font_style.to_sym != font_style
        prose_opts[:first_line_options] = { styles: [font_style, line1_font_style.to_sym] }
      end
      # FIXME make this cleaner!!
      if node.blocks?
        node.blocks.each do |child|
          # FIXME is playback necessary here?
          child.document.playback_attributes child.attributes
          if child.context == :paragraph
            layout_prose child.content, prose_opts
            prose_opts.delete :first_line_options
          else
            # FIXME this could do strange things if the wrong kind of content shows up
            convert_content_for_block child
          end
        end
      elsif node.content_model != :compound && (string = node.content)
        layout_prose string, prose_opts
      end
    end
  end
  # QUESTION should we be adding margin below the abstract??
  #theme_margin :block, :bottom
end
convert_admonition(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 608
def convert_admonition node
  add_dest_for_block node if node.id
  theme_margin :block, :top
  type = node.attr 'name'
  label_align = (@theme.admonition_label_align || :center).to_sym
  # TODO allow vertical_align to be a number
  if (label_valign = (@theme.admonition_label_vertical_align || :middle).to_sym) == :middle
    label_valign = :center
  end
  if (label_min_width = @theme.admonition_label_min_width)
    label_min_width = label_min_width.to_f
  end
  icons = ((doc = node.document).attr? 'icons') ? (doc.attr 'icons') : false
  if (data_uri_enabled = doc.attr? 'data-uri')
    doc.remove_attr 'data-uri'
  end
  if icons == 'font' && !(node.attr? 'icon', nil, false)
    icon_data = admonition_icon_data(label_text = type.to_sym)
    label_width = label_min_width ? label_min_width : (icon_data[:size] * 1.5)
  # NOTE icon_uri will consider icon attribute on node first, then type
  elsif icons && ::File.readable?(icon_path = (node.icon_uri type))
    icons = true
    # TODO introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
    label_width = label_min_width ? label_min_width : 36.0
  else
    if icons
      icons = false
      logger.warn %(admonition icon image not found or not readable: #{icon_path}) unless scratch?
    end
    label_text = node.caption
    theme_font :admonition_label do
      theme_font %(admonition_label_#{type}) do
        if (transform = @text_transform) && transform != 'none'
          label_text = transform_text label_text, transform
        end
        label_width = rendered_width_of_string label_text
        label_width = label_min_width if label_min_width && label_min_width > label_width
      end
    end
  end
  doc.set_attr 'data-uri', '' if data_uri_enabled
  unless ::Array === (cpad = @theme.admonition_padding)
    cpad = ::Array.new 4, cpad
  end
  unless ::Array === (lpad = @theme.admonition_label_padding || cpad)
    lpad = ::Array.new 4, lpad
  end
  # FIXME this shift stuff is a real hack until we have proper margin collapsing
  shift_base = @theme.prose_margin_bottom
  shift_top = shift_base / 3.0
  shift_bottom = (shift_base * 2) / 3.0
  keep_together do |box_height = nil|
    push_scratch doc if scratch?
    pad_box [0, cpad[1], 0, lpad[3]] do
      if box_height
        if (rule_color = @theme.admonition_column_rule_color) &&
            (rule_width = @theme.admonition_column_rule_width || @theme.base_border_width) && rule_width > 0
          float do
            bounding_box [0, cursor], width: label_width + lpad[1], height: box_height do
              stroke_vertical_rule rule_color,
                  at: bounds.right,
                  line_style: (@theme.admonition_column_rule_style || :solid).to_sym,
                  line_width: rule_width
            end
          end
        end
        float do
          bounding_box [0, cursor], width: label_width, height: box_height do
            if icons == 'font'
              # FIXME we're assume icon is a square
              icon_size = fit_icon_to_bounds icon_data[:size]
              # NOTE Prawn's vertical center is not reliable, so calculate it manually
              if label_valign == :center
                label_valign = :top
                if (vcenter_pos = (box_height - icon_size) * 0.5) > 0
                  move_down vcenter_pos
                end
              end
              icon icon_data[:name],
                  valign: label_valign,
                  align: label_align,
                  color: icon_data[:stroke_color],
                  size: icon_size
            elsif icons
              if icon_path.end_with? '.svg'
                begin
                  svg_obj = ::Prawn::SVG::Interface.new ::File.read(icon_path), self,
                          position: label_align,
                      vposition: label_valign,
                      width: label_width,
                      height: box_height,
                      fallback_font_name: default_svg_font,
                      enable_web_requests: (doc.attr? 'allow-uri-read'),
                      enable_file_requests_with_root: (::File.dirname icon_path)
                  if (icon_height = (svg_size = svg_obj.document.sizing).output_height) > box_height
                    icon_width = (svg_obj.resize height: (icon_height = box_height)).output_width
                  else
                    icon_width = svg_size.output_width
                  end
                  svg_obj.draw
                rescue
                  logger.warn %(could not embed admonition icon image: #{icon_path}; #{$!.message})
                end
              else
                begin
                  image_obj, image_info = build_image_object icon_path
                  icon_aspect_ratio = image_info.width.fdiv image_info.height
                  # NOTE don't scale image up if smaller than label_width
                  icon_width = [(to_pt image_info.width, :px), label_width].min
                  if (icon_height = icon_width * (1 / icon_aspect_ratio)) > box_height
                    icon_width *= box_height / icon_height
                    icon_height = box_height
                  end
                  embed_image image_obj, image_info, width: icon_width, position: label_align, vposition: label_valign
                rescue
                  # QUESTION should we show the label in this case?
                  logger.warn %(could not embed admonition icon image: #{icon_path}; #{$!.message})
                end
              end
            else
              # IMPORTANT the label must fit in the alotted space or it shows up on another page!
              # QUESTION anyway to prevent text overflow in the case it doesn't fit?
              theme_font :admonition_label do
                theme_font %(admonition_label_#{type}) do
                  # NOTE Prawn's vertical center is not reliable, so calculate it manually
                  if label_valign == :center
                    label_valign = :top
                    if (vcenter_pos = (box_height - (height_of_typeset_text label_text, line_height: 1)) * 0.5) > 0
                      move_down vcenter_pos
                    end
                  end
                  @text_transform = nil # already applied to label
                  layout_prose label_text,
                      align: label_align,
                      valign: label_valign,
                      line_height: 1,
                      margin: 0,
                      inline_format: false
                end
              end
            end
          end
        end
      end
      pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]] do
        move_down shift_top
        layout_caption node.title if node.title?
        theme_font :admonition do
          convert_content_for_block node
        end
        # FIXME HACK compensate for margin bottom of admonition content
        move_up shift_bottom unless at_page_top?
      end
    end
    pop_scratch doc if scratch?
  end
  theme_margin :block, :bottom
end
convert_audio(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1380
def convert_audio node
  add_dest_for_block node if node.id
  theme_margin :block, :top
  audio_path = node.media_uri(node.attr 'target')
  play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
  layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
  layout_caption node, side: :bottom if node.title?
  theme_margin :block, :bottom
end
convert_colist(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 935
def convert_colist node
  # HACK undo the margin below previous listing or literal block
  # TODO allow this to be set using colist_margin_top
  unless at_page_top?
    # NOTE this logic won't work for a colist nested inside a list item until Asciidoctor 1.5.3
    if (self_idx = node.parent.blocks.index node) && self_idx > 0 &&
        [:listing, :literal].include?(node.parent.blocks[self_idx - 1].context)
      move_up @theme.block_margin_bottom / 2.0
      # or we could do...
      #move_up @theme.block_margin_bottom
      #move_down @theme.caption_margin_inside * 2
    end
  end
  add_dest_for_block node if node.id
  @list_numerals ||= []
  # FIXME move \u2460 to constant (or theme setting)
  # \u2460 = circled one, \u24f5 = double circled one, \u278b = negative circled one
  @list_numerals << %(\u2460)
  #stroke_horizontal_rule @theme.caption_border_bottom_color
  line_metrics = calc_line_metrics @theme.base_line_height
  node.items.each_with_index do |item, idx|
    # FIXME extract to an ensure_space (or similar) method; simplify
    advance_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top) + 1
    convert_colist_item item
  end
  @list_numerals.pop
  # correct bottom margin of last item
  list_margin_bottom = @theme.prose_margin_bottom
  margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
end
convert_colist_item(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 966
def convert_colist_item node
  marker_width = nil
  theme_font :conum do
    marker_width = rendered_width_of_string %(#{conum_glyph 1}x)
    float do
      bounding_box [0, cursor], width: marker_width do
        @list_numerals << (index = @list_numerals.pop).next
        theme_font :conum do
          layout_prose index, align: :center, line_height: @theme.conum_line_height, inline_format: false, margin: 0
        end
      end
    end
  end

  indent marker_width do
    convert_content_for_list_item node, :colist, margin_bottom: @theme.outline_list_item_spacing
  end
end
convert_content_for_block(node, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 136
def convert_content_for_block node, opts = {}
  if self != (prev_converter = node.document.converter)
    node.document.instance_variable_set :@converter, self
  else
    prev_converter = nil
  end
  if node.blocks?
    node.content
  elsif node.content_model != :compound && (string = node.content)
    # TODO this content could be cached on repeat invocations!
    layout_prose string, opts
  end
  node.document.instance_variable_set :@converter, prev_converter if prev_converter
end
convert_content_for_list_item(node, list_type, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1221
def convert_content_for_list_item node, list_type, opts = {}
  if list_type == :dlist # qanda
    terms, desc = node
    [*terms].each {|term| layout_prose %(<em>#{term.text}</em>), opts }
    if desc
      layout_prose desc.text, opts if desc.text?
      convert_content_for_block desc
    end
  else
    layout_prose node.text, opts if node.text?
    convert_content_for_block node
  end
end
convert_dlist(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 985
def convert_dlist node
  add_dest_for_block node if node.id

  case node.style
  when 'qanda'
    (@list_numerals ||= []) << '1'
    convert_outline_list node
    @list_numerals.pop
  else
    # TODO check if we're within one line of the bottom of the page
    # and advance to the next page if so (similar to logic for section titles)
    layout_caption node.title if node.title?

    node.items.each do |terms, desc|
      terms = [*terms]
      # NOTE don't orphan the terms, allow for at least one line of content
      # FIXME extract ensure_space (or similar) method
      advance_page if cursor < @theme.base_line_height_length * (terms.size + 1)
      terms.each do |term|
        # FIXME layout_prose should pass style downward when parsing formatted text
        #layout_prose term.text, style: @theme.description_list_term_font_style.to_sym, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left
        term_text = term.text
        case @theme.description_list_term_font_style.to_sym
        when :bold
          term_text = %(<strong>#{term_text}</strong>)
        when :italic
          term_text = %(<em>#{term_text}</em>)
        when :bold_italic
          term_text = %(<strong><em>#{term_text}</em></strong>)
        end
        layout_prose term_text, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left
      end
      if desc
        indent @theme.description_list_description_indent do
          convert_content_for_list_item desc, :dlist_desc
        end
      end
    end
  end
end
convert_document(doc) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 151
def convert_document doc
  init_pdf doc
  # set default value for pagenums if not otherwise set
  unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
    doc.attributes['pagenums'] = ''
  end
  if (idx_sect = doc.sections.find {|candidate| candidate.sectname == 'index' }) && idx_sect.numbered
    idx_sect.numbered = false
  end unless @capabilities[:special_sectnums]
  #assign_missing_section_ids doc

  # promote anonymous preface (defined using preamble block) to preface section
  # FIXME this should be done in core
  if doc.doctype == 'book' && (blk_0 = doc.blocks[0]) && blk_0.context == :preamble && blk_0.title? &&
      !blk_0.title.nil_or_empty? && blk_0.blocks[0].style != 'abstract' && (blk_1 = doc.blocks[1]) && blk_1.context == :section
    preface = Section.new doc, blk_1.level, false, attributes: { 1 => 'preface', 'style' => 'preface' }
    preface.special = true
    preface.sectname = 'preface'
    preface.title = blk_0.instance_variable_get :@title
    # QUESTION should ID be generated from raw or converted title? core is not clear about this
    preface.id = preface.generate_id
    preface.blocks.replace blk_0.blocks.map {|b| b.parent = preface; b }
    doc.blocks[0] = preface
    blk_0 = blk_1 = preface = nil
  end

  # NOTE on_page_create is called within a float context
  # NOTE on_page_create is not called for imported pages, front and back cover pages, and other image pages
  on_page_create do
    # NOTE we assume in prepress that physical page number reflects page side
    if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_side]) != page_margin
      set_page_margin next_page_margin
    end
    # TODO implement as a watermark (on top)
    if @page_bg_image
      # FIXME implement fitting and centering for SVG
      # TODO implement image scaling (numeric value or "fit")
      canvas { image @page_bg_image, position: :center, fit: [bounds.width, bounds.height] }
    elsif @page_bg_color && @page_bg_color != 'FFFFFF'
      fill_absolute_bounds @page_bg_color
    end
  end if respond_to? :on_page_create

  layout_cover_page :front, doc
  if (insert_title_page = doc.doctype == 'book' || (doc.attr? 'title-page'))
    layout_title_page doc
    # NOTE a new page will already be started if the cover image is a PDF
    start_new_page unless page_is_empty?
  else
    # NOTE a new page will already be started if the cover image is a PDF
    start_new_page unless page_is_empty?
    body_start_page_number = page_number
    if doc.header? && !doc.notitle
      theme_font :heading, level: 1 do
        layout_heading doc.doctitle, align: (@theme.heading_h1_align || :center).to_sym, level: 1
      end
      toc_start = @y
    end
  end

  # NOTE font must be set before toc dry run to ensure dry run size is accurate
  font @theme.base_font_family, size: @theme.base_font_size, style: @theme.base_font_style.to_sym

  num_toc_levels = (doc.attr 'toclevels', 2).to_i
  if (insert_toc = (doc.attr? 'toc') && doc.sections?)
    start_new_page if @ppbook && verso_page?
    toc_page_nums = page_number
    toc_end = nil
    dry_run do
      toc_page_nums = layout_toc doc, num_toc_levels, toc_page_nums, 0, toc_start
      move_down @theme.block_margin_bottom unless insert_title_page
      toc_end = @y
    end
    # NOTE reserve pages for the toc; leaves cursor on page after last page in toc
    if insert_title_page
      toc_page_nums.each { start_new_page }
    else
      (toc_page_nums.first...toc_page_nums.last).each { start_new_page }
      @y = toc_end
    end
  end

  # FIXME only apply to book doctype once title and toc are moved to start page when using article doctype
  #start_new_page if @ppbook && verso_page?
  start_new_page if @media == 'prepress' && verso_page?

  if insert_title_page
    body_offset = (body_start_page_number = page_number) - 1
    front_matter_sig = [@theme.running_content_start_at || 'body', @theme.page_numbering_start_at || 'body', insert_toc]
    # NOTE start running content from title or toc, if specified (default: body)
    num_front_matter_pages = {
      ['title', 'title', true] => [0, 0],
      ['title', 'title', false] => [0, 0],
      ['title', 'toc', true] => [0, 1],
      ['title', 'toc', false] => [0, 1],
      ['title', 'body', true] => [0, body_offset],
      ['title', 'body', false] => [0, 1],
      ['toc', 'title', true] => [1, 0],
      ['toc', 'title', false] => [1, 0],
      ['toc', 'toc', true] => [1, 1],
      ['toc', 'toc', false] => [1, 1],
      ['toc', 'body', true] => [1, body_offset],
      ['body', 'title', true] => [body_offset, 0],
      ['body', 'title', false] => [1, 0],
      ['body', 'toc', true] => [body_offset, 1],
    }[front_matter_sig] || [body_offset, body_offset]
  else
    # Q: what if there's only a toc page, but not title?
    num_front_matter_pages = [body_start_page_number - 1] * 2
  end

  @index.start_page_number = num_front_matter_pages[1] + 1
  doc.set_attr 'pdf-anchor', (doc_anchor = derive_anchor_from_id doc.id, 'top')
  add_dest_for_block doc, doc_anchor

  convert_section generate_manname_section doc if doc.doctype == 'manpage' && (doc.attr? 'manpurpose')

  convert_content_for_block doc

  # NOTE for a book, these are leftover footnotes; for an article this is everything
  layout_footnotes doc

  # NOTE delete orphaned page (a page was created but there was no additional content)
  # QUESTION should we delete page if document is empty? (leaving no pages?)
  delete_page if page_is_empty? && page_count > 1

  toc_page_nums = insert_toc ? (layout_toc doc, num_toc_levels, toc_page_nums.first, num_front_matter_pages[1], toc_start) : []

  unless page_count < body_start_page_number
    unless doc.noheader || @theme.header_height.to_f.zero?
      layout_running_content :header, doc, skip: num_front_matter_pages, body_start_page_number: body_start_page_number
    end
    unless doc.nofooter || @theme.footer_height.to_f.zero?
      layout_running_content :footer, doc, skip: num_front_matter_pages, body_start_page_number: body_start_page_number
    end
  end

  add_outline doc, (doc.attr 'outlinelevels', num_toc_levels).to_i, toc_page_nums, num_front_matter_pages[1]
  # TODO allow document (or theme) to override initial view magnification
  # NOTE add 1 to page height to force initial scroll to 0; a nil value also seems to work
  catalog.data[:OpenAction] = dest_fit_horizontally((page_height + 1), state.pages[0]) if state.pages.size > 0
  catalog.data[:ViewerPreferences] = { DisplayDocTitle: true }

  layout_cover_page :back, doc
  nil
end
Also aliased as: convert_embedded
convert_embedded(doc)

NOTE embedded only makes sense if perhaps we are building on an existing Prawn::Document instance; for now, just treat it the same as a full document.

Alias for: convert_document
convert_example(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 767
def convert_example node
  add_dest_for_block node if node.id
  theme_margin :block, :top
  keep_together do |box_height = nil|
    push_scratch node.document if scratch?
    caption_height = node.title? ? (layout_caption node) : 0
    if box_height
      float do
        bounding_box [0, cursor], width: bounds.width, height: box_height - caption_height do
          theme_fill_and_stroke_bounds :example
        end
      end
    end
    pad_box @theme.example_padding do
      theme_font :example do
        convert_content_for_block node
      end
    end
    pop_scratch node.document if scratch?
  end
  theme_margin :block, :bottom
end
convert_floating_title(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 519
def convert_floating_title node
  add_dest_for_block node if node.id
  # QUESTION should we decouple styles from section titles?
  theme_font :heading, level: (hlevel = node.level + 1) do
    layout_heading node.title, align: (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym, level: hlevel
  end
end
convert_horizontal_rule(node)

deprecated

convert_image(node, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1235
def convert_image node, opts = {}
  node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
  target, image_format = node.target_and_format

  if image_format == 'gif' && !(defined? ::GMagick::Image)
    logger.warn %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
    image_path = false
  elsif ::Base64 === target
    image_path = target
  elsif (image_path = resolve_image_path node, target, (opts.fetch :relative_to_imagesdir, true), image_format) &&
      (::File.readable? image_path)
    # NOTE import_page automatically advances to next page afterwards
    # QUESTION should we add destination to top of imported page?
    return import_page image_path, replace: page_is_empty? if image_format == 'pdf'
  else
    logger.warn %(image to embed not found or not readable: #{image_path || target}) unless scratch?
    # QUESTION should we use alt text in this case?
    return if image_format == 'pdf'
    image_path = false
  end

  theme_margin :block, :top unless (pinned = opts[:pinned])

  return on_image_error :missing, node, target, opts unless image_path

  # TODO move this calculation into a method, such as layout_caption node, side: :bottom, dry_run: true
  caption_h = 0
  dry_run do
    move_down 0.0001 # hack to force top margin to be applied
    # NOTE we assume caption fits on a single page, which seems reasonable
    caption_h = layout_caption node, side: :bottom
  end if node.title?

  # TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
  width = resolve_explicit_width node.attributes, (available_w = bounds.width), support_vw: true, use_fallback: true
  # TODO add `to_pt page_width` method to ViewportWidth type
  width = (width.to_f / 100) * page_width if ViewportWidth === width

  alignment = ((node.attr 'align', nil, false) || @theme.image_align).to_sym
  align_to_page = node.option? 'align-to-page'

  begin
    span_page_width_if align_to_page do
      if image_format == 'svg'
        if ::Base64 === image_path
          svg_data = ::Base64.decode64 image_path
          file_request_root = false
        else
          svg_data = ::File.read image_path
          file_request_root = ::File.dirname image_path
        end
        svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
            position: alignment,
            width: width,
            fallback_font_name: default_svg_font,
            enable_web_requests: (node.document.attr? 'allow-uri-read'),
            # TODO enforce jail in safe mode
            enable_file_requests_with_root: file_request_root
        rendered_w = (svg_size = svg_obj.document.sizing).output_width
        if !width && (svg_obj.document.root.attributes.key? 'width')
          # NOTE scale native width & height from px to pt and restrict width to available width
          if (adjusted_w = [available_w, (to_pt rendered_w, :px)].min) != rendered_w
            svg_size = svg_obj.resize width: (rendered_w = adjusted_w)
          end
        end
        # NOTE shrink image so it fits within available space; group image & caption
        if (rendered_h = svg_size.output_height) > (available_h = cursor - caption_h)
          unless pinned || at_page_top?
            advance_page
            available_h = cursor - caption_h
          end
          if rendered_h > available_h
            rendered_w = (svg_size = svg_obj.resize height: (rendered_h = available_h)).output_width
          end
        end
        add_dest_for_block node if node.id
        # NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
        # breakage occurs when running content (stamps) are added to page
        update_colors if graphic_state.color_space.empty?
        # NOTE prawn-svg 0.24.0, 0.25.0, & 0.25.1 didn't restore font after call to draw (see mogest/prawn-svg#80)
        # NOTE cursor advanced automatically
        svg_obj.draw
        if (link = node.attr 'link', nil, false)
          link_box = [(abs_left = svg_obj.position[0] + bounds.absolute_left), y, (abs_left + rendered_w), (y + rendered_h)]
          link_annotation link_box, Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: link.as_pdf }
        end
      else
        # FIXME this code really needs to be better organized!
        # NOTE use low-level API to access intrinsic dimensions; build_image_object caches image data previously loaded
        image_obj, image_info = ::Base64 === image_path ?
            ::StringIO.open((::Base64.decode64 image_path), 'rb') {|fd| build_image_object fd } :
            ::File.open(image_path, 'rb') {|fd| build_image_object fd }
        # NOTE if width is not specified, scale native width & height from px to pt and restrict width to available width
        rendered_w, rendered_h = image_info.calc_image_dimensions width: (width || [available_w, (to_pt image_info.width, :px)].min)
        # NOTE shrink image so it fits within available space; group image & caption
        if rendered_h > (available_h = cursor - caption_h)
          unless pinned || at_page_top?
            advance_page
            available_h = cursor - caption_h
          end
          if rendered_h > available_h
            rendered_w, rendered_h = image_info.calc_image_dimensions height: (rendered_h = available_h)
          end
        end
        # NOTE must calculate link position before embedding to get proper boundaries
        if (link = node.attr 'link', nil, false)
          img_x, img_y = image_position rendered_w, rendered_h, position: alignment
          link_box = [img_x, (img_y - rendered_h), (img_x + rendered_w), img_y]
        end
        image_top = cursor
        add_dest_for_block node if node.id
        # NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
        # breakage occurs when running content (stamps) are added to page
        update_colors if graphic_state.color_space.empty?
        # NOTE specify both width and height to avoid recalculation
        embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
        link_annotation link_box, Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: link.as_pdf } if link
        # NOTE Asciidoctor disables automatic advancement of cursor for raster images, so move cursor manually
        move_down rendered_h if cursor == image_top
      end
    end
    layout_caption node, side: :bottom if node.title?
    theme_margin :block, :bottom unless pinned
  rescue
    on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{$!.message}))
  end
ensure
  unlink_tmp_file image_path if image_path
end
convert_index_list_item(term) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2078
def convert_index_list_item term
  text = escape_xml term.name
  unless term.container?
    if @media == 'screen'
      pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
    else
      pagenums = consolidate_ranges term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page].to_s }
    end
    text = %(#{text}, #{pagenums * ', '})
  end
  layout_prose text, align: :left, margin: 0

  term.subterms.each do |subterm|
    indent @theme.description_list_description_indent do
      convert_index_list_item subterm
    end
  end unless term.leaf?
end
convert_index_section(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2051
def convert_index_section node
  unless @index.empty?
    space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
    column_box [0, cursor], columns: 2, width: bounds.width, reflow_margins: true do
      @index.categories.each do |category|
        # NOTE cursor method always returns 0 inside column_box; breaks reference_bounds.move_past_bottom
        bounds.move_past_bottom if space_needed_for_category > y - reference_bounds.absolute_bottom
        layout_prose category.name,
          align: :left,
          inline_format: false,
          margin_top: 0,
          margin_bottom: @theme.description_list_term_spacing,
          style: @theme.description_list_term_font_style.to_sym
        category.terms.each do |term|
          convert_index_list_item term
        end
        if @theme.prose_margin_bottom > y - reference_bounds.absolute_bottom
          bounds.move_past_bottom
        else
          move_down @theme.prose_margin_bottom
        end
      end
    end
  end
  nil
end
convert_inline_anchor(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2097
def convert_inline_anchor node
  case node.type
  when :link
    attrs = []
    #attrs << %( id="#{node.id}") if node.id
    if (role = node.role)
      attrs << %( class="#{role}")
    end
    #attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
    attrs << %( target="#{node.attr 'window'}") if node.attr? 'window', nil, false
    if (role = node.attr 'role', nil, false) && (role == 'bare' || ((role.split ' ').include? 'bare'))
      # QUESTION should we insert breakable chars into URI when building fragment instead?
      %(<a href="#{node.target}"#{attrs.join}>#{breakable_uri node.text}</a>)
    # NOTE @media may not be initialized if method is called before convert phase
    elsif (@media ||= node.document.attr 'media', 'screen') != 'screen' || (node.document.attr? 'show-link-uri')
      # QUESTION should we insert breakable chars into URI when building fragment instead?
      # TODO allow style of printed link to be controlled by theme
      %(<a href="#{target = node.target}"#{attrs.join}>#{node.text}</a> [<font size="0.85em">#{breakable_uri target}</font>&#93;)
    else
      %(<a href="#{node.target}"#{attrs.join}>#{node.text}</a>)
    end
  when :xref
    # NOTE non-nil path indicates this is an inter-document xref that's not included in current document
    if (path = node.attributes['path'])
      # NOTE we don't use local as that doesn't work on the web
      # NOTE for the fragment to work in most viewers, it must be #page=<N> <= document this!
      %(<a href="#{node.target}">#{node.text || path}</a>)
    elsif (refid = node.attributes['refid'])
      unless (text = node.text)
        if (refs = node.document.catalog[:refs])
          if ::Asciidoctor::AbstractNode === (ref = refs[refid])
            text = ref.xreftext((@xrefstyle ||= (node.document.attr 'xrefstyle')))
          end
        else
          # Asciidoctor < 1.5.6
          text = node.document.catalog[:ids][refid]
        end
      end
      %(<a anchor="#{derive_anchor_from_id refid}">#{text || "[#{refid}]"}</a>).gsub ']', '&#93;'
    else
      %(<a anchor="#{node.document.attr 'pdf-anchor'}">#{node.text || '[^top&#93;'}</a>)
    end
  when :ref
    # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
    # NOTE id is used instead of target starting in Asciidoctor 2.0.0
    %(<a name="#{node.target || node.id}">#{DummyText}</a>)
  when :bibref
    # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
    # NOTE technically node.text should be node.reftext, but subs have already been applied to text
    # NOTE reftext is no longer enclosed in [] starting in Asciidoctor 2.0.0
    # NOTE id is used instead of target starting in Asciidoctor 2.0.0
    if (reftext = node.reftext)
      reftext = %([#{reftext}]) unless reftext.start_with? '['
    else
      reftext = %([#{node.target || node.id}])
    end
    %(<a name="#{node.target || node.id}">#{DummyText}</a>#{reftext})
  else
    logger.warn %(unknown anchor type: #{node.type.inspect})
  end
end
convert_inline_break(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2159
def convert_inline_break node
  %(#{node.text}<br>)
end
convert_inline_button(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2163
def convert_inline_button node
  %(<strong>[#{NarrowNoBreakSpace}#{node.text}#{NarrowNoBreakSpace}]</strong>)
  #%(<strong>[#{NoBreakSpace}#{node.text}#{NoBreakSpace}]</strong>)
end
convert_inline_callout(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2168
def convert_inline_callout node
  if (conum_color = @theme.conum_font_color)
    # NOTE CMYK value gets flattened here, but is restored by formatted text parser
    %(<color rgb="#{conum_color}">#{conum_glyph node.text.to_i}</color>)
  else
    conum_glyph node.text.to_i
  end
end
convert_inline_footnote(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2177
def convert_inline_footnote node
  if (index = node.attr 'index') && (node.document.footnotes.find {|fn| fn.index == index })
    anchor = node.type == :xref ? '' : %(<a name="_footnoteref_#{index}">#{DummyText}</a>)
    %(#{anchor}<sup>[<a anchor="_footnotedef_#{index}">#{index}</a>]</sup>)
  elsif node.type == :xref
    # NOTE footnote reference not found
    %( <color rgb="FF0000">[#{node.text}]</color>)
  end
end
convert_inline_icon(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2187
def convert_inline_icon node
  if node.document.attr? 'icons', 'font'
    if (icon_name = node.target).include? '@'
      icon_name, icon_set = icon_name.split '@', 2
    else
      icon_set = node.attr 'set', (node.document.attr 'icon-set', 'fa'), false
    end
    icon_set = 'fa' unless IconSets.include? icon_set
    if node.attr? 'size', nil, false
      case (size = node.attr 'size')
      when 'lg'
        size_attr = %( size="1.333em")
      when 'fw'
        size_attr = %( width="1em" align="center")
      else
        size_attr = %( size="#{size.sub 'x', 'em'}")
      end
    else
      size_attr = ''
    end
    begin
      if icon_set == 'fa'
        font_data = nil
        resolved_icon_set = FontAwesomeIconSets.find {|candidate| (font_data = icon_font_data candidate).unicode icon_name rescue nil }
        if resolved_icon_set
          icon_set = resolved_icon_set
          logger.info { %(#{icon_name} icon found in deprecated fa icon set; use #{icon_set} icon set instead) }
        else
          raise
        end
      else
        font_data = icon_font_data icon_set
      end
      # TODO support rotate and flip attributes
      %(<font name="#{icon_set}"#{size_attr}>#{font_data.unicode icon_name}</font>)
    rescue
      logger.warn %(#{icon_name} is not a valid icon name in the #{icon_set} icon set)
      %([#{node.attr 'alt'}])
    end
  else
    %([#{node.attr 'alt'}])
  end
end
convert_inline_image(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2231
def convert_inline_image node
  if node.type == 'icon'
    convert_inline_icon node
  else
    node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
    target, image_format = node.target_and_format
    if image_format == 'gif' && !(defined? ::GMagick::Image)
      logger.warn %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
      img = %([#{node.attr 'alt'}])
    # NOTE an image with a data URI is handled using a temporary file
    elsif (image_path = resolve_image_path node, target, true, image_format) && (::File.readable? image_path)
      width_attr = (width = preresolve_explicit_width node.attributes) ? %( width="#{width}") : nil
      img = %(<img src="#{image_path}" format="#{image_format}" alt="[#{encode_quotes node.attr 'alt'}]"#{width_attr} tmp="#{TemporaryPath === image_path}">)
    else
      logger.warn %(image to embed not found or not readable: #{image_path || target}) unless scratch?
      img = %([#{node.attr 'alt'}])
    end
    (node.attr? 'link', nil, false) ? %(<a href="#{node.attr 'link'}">#{img}</a>) : img
  end
end
convert_inline_indexterm(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2252
def convert_inline_indexterm node
  # NOTE indexterms not supported if text gets substituted before PDF is initialized
  return '' unless instance_variable_defined? :@index
  if scratch?
    node.type == :visible ? node.text : ''
  else
    dest = {
      anchor: (anchor_name = @index.next_anchor_name)
      # NOTE page number is added in InlineDestinationMarker
    }
    anchor = %(<a name="#{anchor_name}" type="indexterm">#{DummyText}</a>)
    if node.type == :visible
      @index.store_primary_term(sanitize(visible_term = node.text), dest)
      %(#{anchor}#{visible_term})
    else
      @index.store_term((node.attr 'terms').map {|term| sanitize term }, dest)
      anchor
    end
  end
end
convert_inline_kbd(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2273
def convert_inline_kbd node
  if (keys = node.attr 'keys').size == 1
    %(<code>#{keys[0]}</code>)
  else
    keys.map {|key| %(<code>#{key}</code>+) }.join.chop
  end
end
convert_inline_menu(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2281
def convert_inline_menu node
  menu = node.attr 'menu'
  caret = (load_theme node.document).menu_caret_content || %( \u203a )
  if !(submenus = node.attr 'submenus').empty?
    %(<strong>#{[menu, *submenus, (node.attr 'menuitem')] * caret}</strong>)
  elsif (menuitem = node.attr 'menuitem')
    %(<strong>#{menu}#{caret}#{menuitem}</strong>)
  else
    %(<strong>#{menu}</strong>)
  end
end
convert_inline_quoted(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2293
def convert_inline_quoted node
  case node.type
  when :emphasis
    open, close, is_tag = ['<em>', '</em>', true]
  when :strong
    open, close, is_tag = ['<strong>', '</strong>', true]
  when :monospaced
    open, close, is_tag = ['<code>', '</code>', true]
  when :superscript
    open, close, is_tag = ['<sup>', '</sup>', true]
  when :subscript
    open, close, is_tag = ['<sub>', '</sub>', true]
  when :double
    open, close, is_tag = ['“', '”', false]
  when :single
    open, close, is_tag = ['‘', '’', false]
  #when :asciimath, :latexmath
  else
    open, close, is_tag = [nil, nil, false]
  end

  if (role = node.role)
    if is_tag
      quoted_text = %(#{open.chop} class="#{role}">#{node.text}#{close})
    else
      quoted_text = %(<span class="#{role}">#{open}#{node.text}#{close}</span>)
    end
  else
    quoted_text = %(#{open}#{node.text}#{close})
  end

  # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
  node.id ? %(<a name="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
end
convert_listing(node)
convert_listing_or_literal(node) click to toggle source

QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?

# File lib/asciidoctor-pdf/converter.rb, line 1435
def convert_listing_or_literal node
  add_dest_for_block node if node.id

  # HACK disable built-in syntax highlighter; must be done before calling node.content!
  if node.style == 'source' && node.attributes['language'] &&
      (highlighter = node.document.attributes['source-highlighter']) && (SourceHighlighters.include? highlighter) &&
      (@capabilities[:syntax_highlighter] ? node.document.syntax_highlighter.highlight? : true)
    case highlighter
    when 'coderay'
      unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
        highlighter = nil if (Helpers.require_library CodeRayRequirePath, 'coderay', :warn).nil?
      end
    when 'pygments'
      unless defined? ::Pygments
        highlighter = nil if (Helpers.require_library 'pygments', 'pygments.rb', :warn).nil?
      end
    when 'rouge'
      unless defined? ::Rouge::Formatters::Prawn
        highlighter = nil if (Helpers.require_library RougeRequirePath, 'rouge', :warn).nil?
      end
    end
    prev_subs = (subs = node.subs).dup
    # NOTE the highlight sub is only set for coderay, rouge, and pygments atm
    highlight_idx = subs.index :highlight
    # NOTE scratch? here only applies if listing block is nested inside another block
    if !highlighter || scratch?
      highlighter = nil
      if highlight_idx
        # switch the :highlight sub back to :specialcharacters
        subs[highlight_idx] = :specialcharacters
      else
        prev_subs = nil
      end
      source_string = preserve_indentation node.content, (node.attr 'tabsize')
    else
      # NOTE the source highlighter logic below handles the callouts and highlight subs
      if highlight_idx
        subs.delete_all :highlight, :callouts
      else
        subs.delete_all :specialcharacters, :callouts
      end
      # the indent guard will be added by the source highlighter logic
      source_string = preserve_indentation node.content, (node.attr 'tabsize'), false
    end
  else
    highlighter = nil
    prev_subs = nil
    source_string = preserve_indentation node.content, (node.attr 'tabsize')
  end

  bg_color_override = nil

  source_chunks = case highlighter
  when 'coderay'
    source_string, conum_mapping = extract_conums source_string
    srclang = node.attr 'language', 'text', false
    begin
      ::CodeRay::Scanners[(srclang = (srclang.start_with? 'html+') ? srclang[5..-1].to_sym : srclang.to_sym)]
    rescue ::ArgumentError
      srclang = :text
    end
    fragments = (::CodeRay.scan source_string, srclang).to_prawn
    conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
  when 'pygments'
    lexer = ::Pygments::Lexer.find_by_alias(node.attr 'language', 'text', false) || ::Pygments::Lexer.find_by_mimetype('text/plain')
    lexer_opts = {
      nowrap: true,
      noclasses: true,
      stripnl: false,
      style: (style = (node.document.attr 'pygments-style') || 'pastie')
    }
    lexer_opts[:startinline] = !(node.option? 'mixed') if lexer.name == 'PHP'
    # TODO enable once we support background color on spans
    #if node.attr? 'highlight', nil, false
    #  unless (hl_lines = node.resolve_lines_to_highlight(node.attr 'highlight', nil, false)).empty?
    #    pygments_config[:hl_lines] = hl_lines * ' '
    #  end
    #end
    # QUESTION should we treat white background as inherit?
    # QUESTION allow border color to be set by theme for highlighted block?
    if (node.document.attr? 'pygments-bgcolor')
      bg_color_override = node.document.attr 'pygments-bgcolor'
    elsif style == 'pastie'
      node.document.set_attr 'pygments-bgcolor', (bg_color_override = nil)
    else
      node.document.set_attr 'pygments-bgcolor',
          (bg_color_override = PygmentsBgColorRx =~ (::Pygments.css '.highlight', style: style) ? $1 : nil)
    end
    source_string, conum_mapping = extract_conums source_string
    # NOTE pygments.rb strips trailing whitespace; preserve it in case there are conums on last line
    num_trailing_spaces = source_string.length - (source_string = source_string.rstrip).length if conum_mapping
    # NOTE highlight can return nil if something goes wrong; fallback to encoded source string if this happens
    result = (lexer.highlight source_string, options: lexer_opts) || (node.apply_subs source_string, [:specialcharacters])
    if (linenums = node.attr? 'linenums')
      linenums = (node.attr 'start', 1, false).to_i
      @theme.code_linenum_font_color ||= '999999'
      conum_mapping ||= {}
    end
    fragments = text_formatter.format result
    fragments = restore_conums fragments, conum_mapping, num_trailing_spaces, linenums if conum_mapping
    fragments = guard_indentation fragments
  when 'rouge'
    lexer = ::Rouge::Lexer.find(node.attr 'language', 'text', false) || ::Rouge::Lexers::PlainText
    lexer_opts = lexer.tag == 'php' ? { start_inline: !(node.option? 'mixed') } : {}
    formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap)
    formatter_opts = (node.attr? 'linenums') ? { line_numbers: true, start_line: (node.attr 'start', 1, false).to_i } : {}
    # QUESTION allow border color to be set by theme for highlighted block?
    bg_color_override = formatter.background_color
    source_string, conum_mapping = extract_conums source_string
    fragments = formatter.format((lexer.lex source_string, lexer_opts), formatter_opts)
    # NOTE cleanup trailing endline (handled in rouge_ext/formatters/prawn instead)
    #fragments[-1][:text] == LF ? fragments.pop : fragments[-1][:text].chop!
    conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
  else
    # NOTE only format if we detect a need (callouts or inline formatting)
    (XMLMarkupRx.match? source_string) ? (text_formatter.format source_string) : [{ text: source_string }]
  end

  node.subs.replace prev_subs if prev_subs

  adjusted_font_size = ((node.option? 'autofit') || (node.document.attr? 'autofit-option')) ?
      (theme_font_size_autofit source_chunks, :code) : nil

  theme_margin :block, :top

  keep_together do |box_height = nil|
    caption_height = node.title? ? (layout_caption node) : 0
    theme_font :code do
      if box_height
        float do
          # TODO move the multi-page logic to theme_fill_and_stroke_bounds
          unless (b_width = @theme.code_border_width || 0) == 0
            b_radius = (@theme.code_border_radius || 0) + b_width
            b_gap_color = bg_color_override || @theme.code_background_color || @page_bg_color
          end
          remaining_height = box_height - caption_height
          i = 0
          while remaining_height > 0
            advance_page if (started_new_page = i > 0)
            fill_height = [remaining_height, cursor].min
            bounding_box [0, cursor], width: bounds.width, height: fill_height do
              theme_fill_and_stroke_bounds :code, background_color: bg_color_override
              unless b_width == 0
                indent b_radius, b_radius do
                  # dashed line to indicate continuation from previous page
                  stroke_horizontal_rule b_gap_color, line_width: b_width, line_style: :dashed
                end if started_new_page
                if remaining_height > fill_height
                  move_down fill_height
                  indent b_radius, b_radius do
                    # dashed line to indicate continuation on next page
                    stroke_horizontal_rule b_gap_color, line_width: b_width, line_style: :dashed
                  end
                end
              end
            end
            remaining_height -= fill_height
            i += 1
          end
        end
      end

      pad_box @theme.code_padding do
        typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height),
            # QUESTION should we require the code_font_color to be set?
            color: (@theme.code_font_color || @font_color),
            size: adjusted_font_size
      end
    end
  end
  stroke_horizontal_rule @theme.caption_border_bottom_color if node.title? && @theme.caption_border_bottom_color

  theme_margin :block, :bottom
end
Also aliased as: convert_listing, convert_literal
convert_literal(node)
convert_olist(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1026
def convert_olist node
  add_dest_for_block node if node.id
  @list_numerals ||= []
  # TODO move list_number resolve to a method
  list_number = case node.style
  when 'arabic'
    '1'
  when 'decimal'
    '01'
  when 'loweralpha'
    'a'
  when 'upperalpha'
    'A'
  when 'lowerroman'
    RomanNumeral.new 'i'
  when 'upperroman'
    RomanNumeral.new 'I'
  when 'lowergreek'
    LowercaseGreekA
  when 'unstyled', 'unnumbered', 'no-bullet'
    nil
  when 'none'
    ''
  else
    '1'
  end
  # TODO support start values < 1 (issue #498)
  if (start = ((node.attr 'start', nil, false) || ((node.option? 'reversed') ? node.items.size : 1)).to_i) > 1
    (start - 1).times { list_number = list_number.next }
  end
  @list_numerals << list_number
  convert_outline_list node
  @list_numerals.pop
end
convert_open(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 790
def convert_open node
  if node.style == 'abstract'
    convert_abstract node
  elsif node.style == 'partintro' && node.blocks.size == 1 && node.blocks[0].style == 'abstract'
    # TODO process block title and id
    # TODO process abstract child even when partintro has multiple blocks
    convert_abstract node.blocks[0]
  else
    add_dest_for_block node if node.id
    layout_caption node.title if node.title?
    convert_content_for_block node
  end
end
convert_outline_list(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1097
def convert_outline_list node
  # TODO check if we're within one line of the bottom of the page
  # and advance to the next page if so (similar to logic for section titles)
  layout_caption node.title if node.title?

  opts = {}
  if (align = resolve_alignment_from_role node.roles)
    opts[:align] = align
  elsif node.style == 'bibliography'
    opts[:align] = :left
  elsif (align = @theme.outline_list_text_align)
    # NOTE theme setting only affects alignment of list text (not nested blocks)
    opts[:align] = align.to_sym
  end

  line_metrics = calc_line_metrics @theme.base_line_height
  complex = false
  # ...or if we want to give all items in the list the same treatment
  #complex = node.items.find(&:complex?) ? true : false
  if (node.context == :ulist && !@list_bullets[-1]) || (node.context == :olist && !@list_numerals[-1])
    if node.style == 'unstyled'
      # unstyled takes away all indentation
      list_indent = 0
    elsif (list_indent = @theme.outline_list_indent) > 0
      # no-bullet aligns text with left-hand side of bullet position (as though there's no bullet)
      list_indent = [list_indent - (rendered_width_of_string %(#{node.context == :ulist ? "\u2022" : '1.'}x)), 0].max
    end
  else
    list_indent = @theme.outline_list_indent
  end
  indent list_indent do
    node.items.each do |item|
      # FIXME extract to an ensure_space (or similar) method; simplify
      advance_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top)
      convert_outline_list_item item, node, opts
    end
  end
  # NOTE Children will provide the necessary bottom margin if last item is complex.
  # However, don't leave gap at the bottom if list is nested in an outline list
  unless complex || (node.nested? && node.parent.parent.outline?)
    # correct bottom margin of last item
    list_margin_bottom = @theme.prose_margin_bottom
    margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
  end
end
convert_outline_list_item(node, list, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1143
def convert_outline_list_item node, list, opts = {}
  # TODO move this to a draw_bullet (or draw_marker) method
  marker_style = {}
  marker_style[:font_color] = @theme.outline_list_marker_font_color || @font_color
  marker_style[:font_family] = font_family
  marker_style[:font_size] = font_size
  marker_style[:line_height] = @theme.base_line_height
  case (list_type = list.context)
  when :ulist
    complex = node.complex?
    if (marker_type = @list_bullets[-1])
      if marker_type == :checkbox
        # QUESTION should we remove marker indent if not a checkbox?
        if node.attr? 'checkbox', nil, false
          marker_type = (node.attr? 'checked', nil, false) ? :checked : :unchecked
          marker = @theme[%(ulist_marker_#{marker_type}_content)] || BallotBox[marker_type]
        end
      else
        marker = @theme[%(ulist_marker_#{marker_type}_content)] || Bullets[marker_type]
      end
      [:font_color, :font_family, :font_size, :line_height].each do |prop|
        marker_style[prop] = @theme[%(ulist_marker_#{marker_type}_#{prop})] || @theme[%(ulist_marker_#{prop})] || marker_style[prop]
      end if marker
    end
  when :olist
    complex = node.complex?
    if (index = @list_numerals.pop)
      if index.empty?
        marker = ''
      else
        marker = %(#{index}.)
        dir = (node.parent.option? 'reversed') ? :pred : :next
        @list_numerals << (index = index.public_send dir)
      end
    end
  when :dlist
    # NOTE list.style is 'qanda'
    complex = node[1] && node[1].complex?
    @list_numerals << (index = @list_numerals.pop).next
    marker = %(#{index}.)
  else
    complex = node.complex?
    logger.warn %(unknown list type #{list_type.inspect})
    marker = @theme.ulist_marker_disc_content || Bullets[:disc]
  end

  if marker
    if marker_style[:font_family] == 'fa'
      logger.info { 'deprecated fa icon set found in theme; use fas, far, or fab instead' }
      marker_style[:font_family] = FontAwesomeIconSets.find {|candidate| (icon_font_data candidate).yaml[candidate].value? marker } || 'fas'
    end
    marker_gap = rendered_width_of_char 'x'
    font marker_style[:font_family], size: marker_style[:font_size] do
      marker_width = rendered_width_of_string marker
      start_position = -marker_width + -marker_gap
      float do
        flow_bounding_box start_position, width: marker_width do
          layout_prose marker,
            align: :right,
            character_spacing: -0.5,
            color: marker_style[:font_color],
            inline_format: false,
            line_height: marker_style[:line_height],
            margin: 0,
            normalize: false,
            single_line: true
        end
      end
    end
  end

  if complex
    convert_content_for_list_item node, list_type, opts
  else
    convert_content_for_list_item node, list_type, (opts.merge margin_bottom: @theme.outline_list_item_spacing)
  end
end
convert_page_break(node) click to toggle source

NOTE to insert sequential page breaks, you must put {nbsp} between page breaks

# File lib/asciidoctor-pdf/converter.rb, line 2035
def convert_page_break node
  unless at_page_top?
    if (page_layout = node.attr 'page-layout').nil_or_empty?
      if node.role? && (page_layout = (node.roles.map(&:to_sym) & PageLayouts)[-1])
        advance_page layout: page_layout
      else
        advance_page
      end
    elsif PageLayouts.include?(page_layout = page_layout.to_sym)
      advance_page layout: page_layout
    else
      advance_page
    end
  end
end
convert_paragraph(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 575
def convert_paragraph node
  add_dest_for_block node if node.id
  prose_opts = { margin_bottom: 0 }
  lead = (roles = node.roles).include? 'lead'
  if (align = resolve_alignment_from_role roles)
    prose_opts[:align] = align
  end

  if (text_indent = @theme.prose_text_indent)
    prose_opts[:indent_paragraphs] = text_indent
  end

  # TODO check if we're within one line of the bottom of the page
  # and advance to the next page if so (similar to logic for section titles)
  layout_caption node.title if node.title?

  if lead
    theme_font :lead do
      layout_prose node.content, prose_opts
    end
  else
    layout_prose node.content, prose_opts
  end

  if (margin_inner_val = @theme.prose_margin_inner) &&
      (next_block = (siblings = node.parent.blocks)[(siblings.index node) + 1]) && next_block.context == :paragraph
    margin_bottom_val = margin_inner_val
  else
    margin_bottom_val = @theme.prose_margin_bottom
  end
  margin_bottom margin_bottom_val
end
convert_preamble(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 566
def convert_preamble node
  # TODO find_by needs to support a depth argument
  # FIXME core should not be promoting paragraph to preamble if there are no sections
  if (first_p = (node.find_by context: :paragraph)[0]) && first_p.parent == node && node.document.sections?
    first_p.add_role 'lead'
  end
  convert_content_for_block node
end
convert_quote(node)
convert_quote_or_verse(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 804
def convert_quote_or_verse node
  add_dest_for_block node if node.id
  theme_margin :block, :top
  b_width = @theme.blockquote_border_width
  b_color = @theme.blockquote_border_color
  keep_together do |box_height = nil|
    push_scratch node.document if scratch?
    start_page_number = page_number
    start_cursor = cursor
    caption_height = node.title? ? (layout_caption node) : 0
    pad_box @theme.blockquote_padding do
      theme_font :blockquote do
        if node.context == :quote
          convert_content_for_block node
        else # verse
          content = preserve_indentation node.content, (node.attr 'tabsize')
          layout_prose content, normalize: false, align: :left
        end
      end
      if node.attr? 'attribution', nil, false
        theme_font :blockquote_cite do
          layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle', nil, false)].compact * ', '}), align: :left, normalize: false
        end
      end
    end
    # FIXME we want to draw graphics before content, but box_height is not reliable when spanning pages
    # FIXME border extends to bottom of content area if block terminates at bottom of page
    if box_height && b_width > 0
      page_spread = page_number - start_page_number + 1
      end_cursor = cursor
      go_to_page start_page_number
      move_cursor_to start_cursor
      page_spread.times do |i|
        if i == 0
          y_draw = cursor
          b_height = page_spread > 1 ? y_draw : (y_draw - end_cursor)
        else
          bounds.move_past_bottom
          y_draw = cursor
          b_height = page_spread - 1 == i ? (y_draw - end_cursor) : y_draw
        end
        # NOTE skip past caption if present
        if caption_height > 0
          if caption_height > cursor
            caption_height -= cursor
            next # keep skipping, caption is on next page
          end
          y_draw -= caption_height
          b_height -= caption_height
          caption_height = 0
        end
        # NOTE b_height is 0 when block terminates at bottom of page
        bounding_box [0, y_draw], width: bounds.width, height: b_height do
          stroke_vertical_rule b_color, line_width: b_width, at: b_width / 2.0
        end unless b_height == 0
      end
    end
    pop_scratch node.document if scratch?
  end
  theme_margin :block, :bottom
end
Also aliased as: convert_quote, convert_verse
convert_section(sect, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 460
def convert_section sect, opts = {}
  if sect.sectname == 'abstract'
    # HACK cheat a bit to hide this section from TOC; TOC should filter these sections
    sect.context = :open
    return convert_abstract sect
  end

  type = nil
  theme_font :heading, level: (hlevel = sect.level + 1) do
    title = sect.numbered_title formal: true
    align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
    if sect.part_or_chapter?
      if sect.chapter?
        type = :chapter
        start_new_chapter sect
      else
        type = :part
        start_new_part sect
      end
    else
      # FIXME smarter calculation here!!
      start_new_page unless at_page_top? || cursor > (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + (@theme.base_line_height_length * 1.5)
    end
    # QUESTION should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
    sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
    # QUESTION should we just assign the section this generated id?
    # NOTE section must have pdf-anchor in order to be listed in the TOC
    sect.set_attr 'pdf-anchor', (sect_anchor = derive_anchor_from_id sect.id, %(#{start_pgnum}-#{y.ceil}))
    add_dest_for_block sect, sect_anchor
    if type == :part
      layout_part_title sect, title, align: align, level: hlevel
    elsif type == :chapter
      layout_chapter_title sect, title, align: align, level: hlevel
    else
      layout_heading title, align: align, level: hlevel
    end
  end

  sect.sectname == 'index' ? (convert_index_section sect) : (convert_content_for_block sect)
  layout_footnotes sect if type == :chapter
  sect.set_attr 'pdf-page-end', page_number
end
convert_sidebar(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 869
def convert_sidebar node
  add_dest_for_block node if node.id
  theme_margin :block, :top
  keep_together do |box_height = nil|
    push_scratch node.document if scratch?
    if box_height
      # FIXME due to the calculation error logged in #789, we must advance page even when content is split across pages
      advance_page if box_height > cursor && !at_page_top?
      float do
        # TODO move the multi-page logic to theme_fill_and_stroke_bounds
        if (b_width = @theme.sidebar_border_width || 0) > 0 && (b_color = @theme.sidebar_border_color)
          if b_color == @page_bg_color # let page background cut into sidebar background
            b_gap_color, b_shift = @page_bg_color, b_width
          elsif (b_gap_color = @theme.sidebar_background_color) && b_gap_color != b_color
            b_shift = 0
          else # let page background cut into border
            b_gap_color, b_shift = @page_bg_color, 0
          end
        else # let page background cut into sidebar background
          b_width = 0.5 if b_width == 0
          b_shift, b_gap_color = b_width * 0.5, @page_bg_color
        end
        b_radius = (@theme.sidebar_border_radius || 0) + b_width
        initial_page, remaining_height = true, box_height
        while remaining_height > 0
          advance_page unless initial_page
          fragment_height = [(available_height = cursor), remaining_height].min
          bounding_box [0, available_height], width: bounds.width, height: fragment_height do
            theme_fill_and_stroke_bounds :sidebar
            unless b_width == 0
              indent b_radius, b_radius do
                move_down b_shift
                # dashed line to indicate continuation from previous page; swell line to cover background
                stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
                move_up b_shift
              end unless initial_page
              if remaining_height > fragment_height
                move_down fragment_height - b_shift
                indent b_radius, b_radius do
                  # dashed line to indicate continuation to next page; swell line to cover background
                  stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
                end
              end
            end
          end
          remaining_height -= fragment_height
          initial_page = false
        end
      end
    end
    pad_box @theme.sidebar_padding do
      if node.title?
        theme_font :sidebar_title do
          # QUESTION should we allow margins of sidebar title to be customized?
          layout_heading node.title, align: (@theme.sidebar_title_align || @base_align).to_sym, margin_top: 0
        end
      end
      theme_font :sidebar do
        convert_content_for_block node
      end
    end
    pop_scratch node.document if scratch?
  end
  theme_margin :block, :bottom
end
convert_table(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1701
def convert_table node
  add_dest_for_block node if node.id
  # TODO we could skip a lot of the logic below when num_rows == 0
  num_rows = node.attr 'rowcount'
  num_cols = node.columns.size
  table_header = false
  theme = @theme

  tbl_bg_color = resolve_theme_color :table_background_color
  # QUESTION should we fallback to page background color? (which is never transparent)
  #tbl_bg_color = resolve_theme_color :table_background_color, @page_bg_color
  # ...and if so, should we try to be helpful and use @page_bg_color for tables nested in blocks?
  #unless tbl_bg_color
  #  tbl_bg_color = @page_bg_color unless [:section, :document].include? node.parent.context
  #end

  # NOTE emulate table bg color by using it as a fallback value for each element
  head_bg_color = resolve_theme_color :table_head_background_color, tbl_bg_color
  foot_bg_color = resolve_theme_color :table_foot_background_color, tbl_bg_color
  body_bg_color = resolve_theme_color :table_body_background_color,
      # table_odd_row_background_color is deprecated
      (resolve_theme_color :table_odd_row_background_color, tbl_bg_color)
  body_stripe_bg_color = resolve_theme_color :table_body_stripe_background_color,
      # table_even_row_background_color is deprecated
      (resolve_theme_color :table_even_row_background_color, tbl_bg_color)

  table_data = []
  node.rows[:head].each do |row|
    table_header = true
    if (head_transform = theme.table_head_text_transform)
      head_transform = nil if head_transform == 'none'
    end
    row_data = []
    row.each do |cell|
      row_data << {
        content: (head_transform ? (transform_text cell.text, head_transform) : cell.text),
        inline_format: [normalize: true],
        background_color: head_bg_color,
        text_color: (theme.table_head_font_color || theme.table_font_color || @font_color),
        size: (theme.table_head_font_size || theme.table_font_size),
        font: (theme.table_head_font_family || theme.table_font_family),
        font_style: (val = theme.table_head_font_style || theme.table_font_style) ? val.to_sym : nil,
        colspan: cell.colspan || 1,
        rowspan: cell.rowspan || 1,
        align: (cell.attr 'halign', nil, false).to_sym,
        valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
      }
    end
    table_data << row_data
  end

  header_cell_data_cache = nil
  (node.rows[:body] + node.rows[:foot]).each do |row|
    row_data = []
    row.each do |cell|
      cell_data = {
        text_color: (theme.table_font_color || @font_color),
        size: theme.table_font_size,
        font: theme.table_font_family,
        colspan: cell.colspan || 1,
        rowspan: cell.rowspan || 1,
        align: (cell.attr 'halign', nil, false).to_sym,
        valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym,
        padding: theme.table_cell_padding
      }
      cell_transform = nil
      case cell.style
      when :emphasis
        cell_data[:font_style] = :italic
        cell_line_metrics = calc_line_metrics theme.base_line_height
      when :strong
        cell_data[:font_style] = :bold
        cell_line_metrics = calc_line_metrics theme.base_line_height
      when :header
        unless header_cell_data_cache
          header_cell_data_cache = {}
          [
            #['align', :align, true], # QUESTION should we honor alignment set by col/cell spec? how can we tell?
            ['font_color', :text_color, false],
            ['font_family', :font, false],
            ['font_size', :size, false],
            ['font_style', :font_style, true],
            ['text_transform', :text_transform, true]
          ].each do |(theme_key, data_key, symbol_value)|
            if (val = theme[%(table_header_cell_#{theme_key})] || theme[%(table_head_#{theme_key})])
              header_cell_data_cache[data_key] = symbol_value ? val.to_sym : val
            end
          end
          if (val = resolve_theme_color :table_header_cell_background_color, head_bg_color)
            header_cell_data_cache[:background_color] = val
          end
        end
        header_cell_data = header_cell_data_cache.dup
        if (cell_transform = header_cell_data.delete :text_transform) == 'none'
          cell_transform = nil
        end
        cell_data.update header_cell_data unless header_cell_data.empty?
        cell_line_metrics = calc_line_metrics theme.base_line_height
      when :monospaced
        cell_data[:font] = theme.literal_font_family
        if (val = theme.literal_font_size)
          cell_data[:size] = val
        end
        if (val = theme.literal_font_color)
          cell_data[:text_color] = val
        end
        cell_line_metrics = calc_line_metrics theme.base_line_height
      when :literal
        # FIXME core should not substitute in this case
        cell_data[:content] = preserve_indentation((cell.instance_variable_get :@text), (node.document.attr 'tabsize'))
        # NOTE the absence of the inline_format option implies it's disabled
        # QUESTION should we use literal_font_*, code_font_*, or introduce another category?
        cell_data[:font] = theme.code_font_family
        if (val = theme.code_font_size)
          cell_data[:size] = val
        end
        if (val = theme.code_font_color)
          cell_data[:text_color] = val
        end
        cell_line_metrics = calc_line_metrics theme.code_line_height
      when :verse
        cell_data[:content] = preserve_indentation cell.text, (node.document.attr 'tabsize')
        cell_data[:inline_format] = true
        cell_line_metrics = calc_line_metrics theme.base_line_height
      when :asciidoc
        asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self,
            (cell_data.merge content: cell.inner_document, font_style: (val = theme.table_font_style) ? val.to_sym : nil)
        cell_data = { content: asciidoc_cell }
      else
        cell_data[:font_style] = (val = theme.table_font_style) ? val.to_sym : nil
        cell_line_metrics = calc_line_metrics theme.base_line_height
      end
      if cell_line_metrics
        if ::Array === (cell_padding = cell_data[:padding]) && cell_padding.size == 4
          cell_padding = cell_padding.dup
        else
          cell_padding = cell_data[:padding] = inflate_padding cell_padding
        end
        cell_padding[0] += cell_line_metrics.padding_top
        cell_padding[2] += cell_line_metrics.padding_bottom
        cell_data[:leading] = cell_line_metrics.leading
        # TODO patch prawn-table to pass through final_gap option
        #cell_data[:final_gap] = cell_line_metrics.final_gap
      end
      unless cell_data.key? :content
        if (cell_text = cell_transform ? (transform_text cell.text, cell_transform) : cell.text).include? LF
          # NOTE effectively the same as calling cell.content (should we use that instead?)
          # FIXME hard breaks not quite the same result as separate paragraphs; need custom cell impl here
          cell_data[:content] = (cell_text.split BlankLineRx).map {|l| l.tr_s WhitespaceChars, ' ' }.join DoubleLF
          cell_data[:inline_format] = true
        else
          cell_data[:content] = cell_text
          cell_data[:inline_format] = [normalize: true]
        end
      end
      row_data << cell_data
    end
    table_data << row_data
  end

  # NOTE Prawn aborts if table data is empty, so ensure there's at least one row
  if table_data.empty?
    empty_row = []
    node.columns.each do
      empty_row << { content: '' }
    end
    table_data = [empty_row]
  end

  border_width = {}
  table_border_color = theme.table_border_color || theme.table_grid_color || theme.base_border_color
  table_border_style = (theme.table_border_style || :solid).to_sym
  table_border_width = theme.table_border_width
  [:top, :bottom, :left, :right].each {|edge| border_width[edge] = table_border_width }
  table_grid_color = theme.table_grid_color || table_border_color
  table_grid_style = (theme.table_grid_style || table_border_style).to_sym
  table_grid_width = theme.table_grid_width || theme.table_border_width
  [:cols, :rows].each {|edge| border_width[edge] = table_grid_width }

  case (grid = node.attr 'grid', 'all', 'table-grid')
  when 'all'
    # keep inner borders
  when 'cols'
    border_width[:rows] = 0
  when 'rows'
    border_width[:cols] = 0
  else # none
    border_width[:rows] = border_width[:cols] = 0
  end

  case (frame = node.attr 'frame', 'all', 'table-frame')
  when 'all'
    # keep outer borders
  when 'topbot', 'ends'
    border_width[:left] = border_width[:right] = 0
  when 'sides'
    border_width[:top] = border_width[:bottom] = 0
  else # none
    border_width[:top] = border_width[:right] = border_width[:bottom] = border_width[:left] = 0
  end

  if node.option? 'autowidth'
    table_width = (node.attr? 'width', nil, false) ? bounds.width * ((node.attr 'tablepcwidth') / 100.0) :
        ((node.has_role? 'spread') ? bounds.width : nil)
    column_widths = []
  else
    table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
    column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * table_width) / 100.0 }
    # NOTE until Asciidoctor 1.5.4, colpcwidth values didn't always add up to 100%; use last column to compensate
    unless column_widths.empty? || (width_delta = table_width - column_widths.reduce(:+)) == 0
      column_widths[-1] += width_delta
    end
  end

  if ((alignment = node.attr 'align', nil, false) && (BlockAlignmentNames.include? alignment)) ||
      (alignment = (node.roles & BlockAlignmentNames)[-1])
    alignment = alignment.to_sym
  else
    alignment = :left
  end

  caption_side = (theme.table_caption_side || :top).to_sym

  table_settings = {
    header: table_header,
    position: alignment,
    cell_style: {
      # NOTE the border color and style of the outer frame is set later
      border_color: table_grid_color,
      border_lines: [table_grid_style],
      # NOTE the border width is set later
      border_width: 0
    },
    width: table_width,
    column_widths: column_widths
  }

  # QUESTION should we support nth; should we support sequence of roles?
  case node.attr 'stripes', nil, 'table-stripes'
  when 'all'
    table_settings[:row_colors] = [body_stripe_bg_color]
  when 'even'
    table_settings[:row_colors] = [body_bg_color, body_stripe_bg_color]
  when 'odd'
    table_settings[:row_colors] = [body_stripe_bg_color, body_bg_color]
  else # none
    table_settings[:row_colors] = [body_bg_color]
  end

  theme_margin :block, :top

  table table_data, table_settings do
    # NOTE call width to capture resolved table width
    table_width = width
    @pdf.layout_table_caption node, table_width, alignment if node.title? && caption_side == :top
    if grid == 'none' && frame == 'none'
      if table_header
        # FIXME allow header border bottom width and style to be set by theme
        rows(0).tap do |r|
          r.border_bottom_line, r.border_bottom_width = :solid, 1.25
          # QUESTION should we use the table border color for the bottom border color of the header row?
          #r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, :solid, 1.25
        end
      end
    else
      # apply the grid setting first across all cells
      cells.border_width = [border_width[:rows], border_width[:cols], border_width[:rows], border_width[:cols]]

      if table_header
        # FIXME allow header border bottom width and style to be set by theme
        rows(0).tap do |r|
          r.border_bottom_line, r.border_bottom_width = :solid, 1.25
          # QUESTION should we use the table border color for the bottom border color of the header row?
          #r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, :solid, 1.25
        end
        rows(1).tap do |r|
          r.border_top_line, r.border_top_width = :solid, 1.25
          # QUESTION should we use the table border color for the top border color of the first row?
          #r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, :solid, 1.25
        end if num_rows > 1
      end

      # top edge of table
      rows(0).tap do |r|
        r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, table_border_style, border_width[:top]
      end
      # right edge of table
      columns(num_cols - 1).tap do |r|
        r.border_right_color, r.border_right_line, r.border_right_width = table_border_color, table_border_style, border_width[:right]
      end
      # bottom edge of table
      rows(num_rows - 1).tap do |r|
        r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, table_border_style, border_width[:bottom]
      end
      # left edge of table
      columns(0).tap do |r|
        r.border_left_color, r.border_left_line, r.border_left_width = table_border_color, table_border_style, border_width[:left]
      end
    end

    # QUESTION should cell padding be configurable for foot row cells?
    unless node.rows[:foot].empty?
      foot_row = row(num_rows - 1)
      foot_row.background_color = foot_bg_color
      # FIXME find a way to do this when defining the cells
      foot_row.text_color = theme.table_foot_font_color if theme.table_foot_font_color
      foot_row.size = theme.table_foot_font_size if theme.table_foot_font_size
      foot_row.font = theme.table_foot_font_family if theme.table_foot_font_family
      foot_row.font_style = theme.table_foot_font_style.to_sym if theme.table_foot_font_style
      # HACK we should do this transformation when creating the cell
      #if (foot_transform = theme.table_foot_text_transform) && foot_transform != 'none'
      #  foot_row.each {|c| c.content = (transform_text c.content, foot_transform) if c.content }
      #end
    end
  end
  layout_table_caption node, table_width, alignment, :bottom if node.title? && caption_side == :bottom
  theme_margin :block, :bottom
end
convert_thematic_break(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2020
def convert_thematic_break node
  theme_margin :thematic_break, :top
  stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width, line_style: @theme.thematic_break_border_style.to_sym
  theme_margin :thematic_break, :bottom
end
Also aliased as: convert_horizontal_rule
convert_toc(node) click to toggle source

NOTE manual placement not yet possible, so return nil

# File lib/asciidoctor-pdf/converter.rb, line 2030
def convert_toc node
  nil
end
convert_ulist(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1061
def convert_ulist node
  add_dest_for_block node if node.id
  # TODO move bullet_type to method on List (or helper method)
  if node.option? 'checklist'
    @list_bullets << :checkbox
  else
    bullet_type = if (style = node.style)
      case style
      when 'bibliography'
        :square
      when 'unstyled', 'no-bullet'
        nil
      else
        if Bullets.key?(candidate = style.to_sym)
          candidate
        else
          logger.warn %(unknown unordered list style: #{candidate})
          :disc
        end
      end
    else
      case node.outline_level
      when 1
        :disc
      when 2
        :circle
      else
        :square
      end
    end
    @list_bullets << bullet_type
  end
  convert_outline_list node
  @list_bullets.pop
end
convert_verse(node)
convert_video(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1390
def convert_video node
  case (poster = node.attr 'poster', nil, false)
  when 'youtube'
    video_path = %(https://www.youtube.com/watch?v=#{video_id = node.attr 'target'})
    # see http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
    poster = (node.document.attr? 'allow-uri-read') ? %(https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg) : nil
    type = 'YouTube video'
  when 'vimeo'
    video_path = %(https://vimeo.com/#{video_id = node.attr 'target'})
    if node.document.attr? 'allow-uri-read'
      if node.document.attr? 'cache-uri'
        Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
      else
        ::OpenURI
      end
      poster = open(%(http://vimeo.com/api/v2/video/#{video_id}.xml), 'r') do |f|
        /<thumbnail_large>(.*?)<\/thumbnail_large>/ =~ f.read && $1
      end
    end
    type = 'Vimeo video'
  else
    video_path = node.media_uri(node.attr 'target')
    type = 'video'
  end

  if poster.nil_or_empty?
    add_dest_for_block node if node.id
    theme_margin :block, :top
    play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
    layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
    layout_caption node, side: :bottom if node.title?
    theme_margin :block, :bottom
  else
    original_attributes = node.attributes.dup
    begin
      node.update_attributes 'target' => poster, 'link' => video_path
      #node.set_attr 'pdfwidth', '100%' unless (node.attr? 'width') || (node.attr? 'pdfwidth')
      convert_image node
    ensure
      node.attributes.replace original_attributes
    end
  end
end
default_svg_font() click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3159
def default_svg_font
  @theme.svg_font_family || @theme.base_font_family
end
derive_anchor_from_id(value, default_value = nil) click to toggle source

Derive a PDF-safe, ASCII-only anchor name from the given value. Encodes value into hex if it contains characters outside the ASCII range. If value is nil, derive an anchor name from the default_value, if given.

# File lib/asciidoctor-pdf/converter.rb, line 3424
def derive_anchor_from_id value, default_value = nil
  if value
    value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
  elsif default_value
    %(__anchor-#{default_value})
  end
end
extract_conums(string) click to toggle source

Extract callout marks from string, indexed by 0-based line number Return an Array with the processed string as the first argument and the mapping of lines to conums as the second.

# File lib/asciidoctor-pdf/converter.rb, line 1616
def extract_conums string
  conum_mapping = {}
  auto_num = 0
  string = string.split(LF).map.with_index {|line, line_num|
    # FIXME we get extra spaces before numbers if more than one on a line
    if line.include? '<'
      line.gsub(CalloutExtractRx) {
        # honor the escape
        if $1 == ?\\
          $&.sub $1, ''
        else
          (conum_mapping[line_num] ||= []) << ($3 == '.' ? (auto_num += 1) : $3.to_i)
          ''
        end
      }
    else
      line
    end
  } * LF
  conum_mapping = nil if conum_mapping.empty?
  [string, conum_mapping]
end
fit_icon_to_bounds(preferred_size = 24) click to toggle source

Reduce icon height to fit inside bounds.height. Icons will not render properly if they are larger than the current bounds.height.

# File lib/asciidoctor-pdf/converter.rb, line 2705
def fit_icon_to_bounds preferred_size = 24
  (max_height = bounds.height) < preferred_size ? max_height : preferred_size
end
font_path(font_file, fonts_dir) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3154
def font_path font_file, fonts_dir
  # resolve relative to built-in font dir unless path is absolute
  ::File.absolute_path font_file, fonts_dir
end
generate_manname_section(node) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2528
def generate_manname_section node
  title = node.attr 'manname-title', 'Name'
  if (next_section = node.sections[0]) && (next_section_title = next_section.title) == next_section_title.upcase
    title = title.upcase
  end
  sect = Section.new node, 1
  sect.sectname = 'section'
  sect.id = node.attr 'manname-id'
  sect.title = title
  sect << (Block.new sect, :paragraph, source: %(#{node.attr 'manname'} - #{node.attr 'manpurpose'}))
  sect
end
guard_indentation(fragments) click to toggle source

Adds guards to preserve indentation

# File lib/asciidoctor-pdf/converter.rb, line 1690
def guard_indentation fragments
  start_of_line = true
  fragments.each do |fragment|
    next if (text = fragment[:text]).empty?
    text[0] = GuardedIndent if start_of_line && (text.start_with? ' ')
    text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
    start_of_line = text.end_with? LF
  end
  fragments
end
height_of_typeset_text(string, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3360
def height_of_typeset_text string, opts = {}
  line_metrics = (calc_line_metrics opts[:line_height] || @theme.base_line_height)
  (height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + line_metrics.padding_bottom
end
init_pdf(doc) click to toggle source

TODO only allow method to be called once (or we need a reset)

# File lib/asciidoctor-pdf/converter.rb, line 304
def init_pdf doc
  theme = load_theme doc
  pdf_opts = build_pdf_options doc, theme
  # QUESTION should page options be preserved (otherwise, not readily available)
  #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
  ::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts
  @page_margin_by_side = { recto: page_margin, verso: page_margin }
  if (@media = doc.attr 'media', 'screen') == 'prepress'
    @ppbook = doc.doctype == 'book'
    page_margin_recto = @page_margin_by_side[:recto]
    if (page_margin_outer = theme.page_margin_outer)
      page_margin_recto[1] = @page_margin_by_side[:verso][3] = page_margin_outer
    end
    if (page_margin_inner = theme.page_margin_inner)
      page_margin_recto[3] = @page_margin_by_side[:verso][1] = page_margin_inner
    end
    # NOTE prepare scratch document to use page margin from recto side (which has same width as verso side)
    set_page_margin page_margin_recto unless page_margin_recto == page_margin
  else
    @ppbook = false
  end
  # QUESTION should ThemeLoader register fonts?
  register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
  if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image != 'none'
    @page_bg_image = bg_image
  else
    @page_bg_image = nil
  end
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
  @fallback_fonts = [*theme.font_fallbacks]
  @font_color = theme.base_font_color
  @base_align = (align = doc.attr 'text-alignment') && (TextAlignmentNames.include? align) ? align : theme.base_align
  @text_transform = nil
  @list_numerals = []
  @list_bullets = []
  @footnotes = []
  @index = IndexCatalog.new
  # NOTE we have to init Pdfmark class here while we have reference to the doc
  @pdfmark = (doc.attr? 'pdfmark') ? (Pdfmark.new doc) : nil
  init_scratch_prototype
  self
end
init_scratch_prototype() click to toggle source

QUESTION move to prawn/extensions.rb?

# File lib/asciidoctor-pdf/converter.rb, line 3656
def init_scratch_prototype
  @save_state = nil
  @scratch_depth = 0
  # IMPORTANT don't set font before using Marshal, it causes serialization to fail
  @prototype = ::Marshal.load ::Marshal.dump self
  @prototype.state.store.info.data[:Scratch] = true
  # NOTE we're now starting a new page each time, so no need to do it here
  #@prototype.start_new_page if @prototype.page_number == 0
end
layout_caption(subject, opts = {}) click to toggle source

Render the caption and return the height of the rendered content TODO allow margin to be zeroed

# File lib/asciidoctor-pdf/converter.rb, line 2543
def layout_caption subject, opts = {}
  mark = { cursor: cursor, page_number: page_number }
  case subject
  when ::String
    string = subject
  when ::Asciidoctor::AbstractBlock
    if subject.title?
      string = subject.captioned_title
    else
      return 0
    end
  else
    return 0
  end
  theme_font :caption do
    if (side = (opts.delete :side) || :top) == :top
      margin = { top: @theme.caption_margin_outside, bottom: @theme.caption_margin_inside }
    else
      margin = { top: @theme.caption_margin_inside, bottom: @theme.caption_margin_outside }
    end
    layout_prose string, {
      margin_top: margin[:top],
      margin_bottom: margin[:bottom],
      align: (@theme.caption_align || @base_align).to_sym,
      normalize: false
    }.merge(opts)
    if side == :top && @theme.caption_border_bottom_color
      stroke_horizontal_rule @theme.caption_border_bottom_color
      # FIXME HACK move down slightly so line isn't covered by filled area (half width of line)
      move_down 0.25
    end
  end
  # NOTE we assume we don't clear more than one page
  if page_number > mark[:page_number]
    mark[:cursor] + (bounds.top - cursor)
  else
    mark[:cursor] - cursor
  end
end
layout_chapter_title(node, title, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2483
def layout_chapter_title node, title, opts = {}
  layout_heading title, opts
end
Also aliased as: layout_part_title
layout_cover_page(face, doc) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2451
def layout_cover_page face, doc
  # TODO turn processing of attribute with inline image a utility function in Asciidoctor
  if (cover_image = (doc.attr %(#{face}-cover-image)))
    if (cover_image.include? ':') && cover_image =~ ImageAttributeValueRx
      # TODO support explicit image format
      cover_image = resolve_image_path doc, $1
    else
      cover_image = resolve_image_path doc, cover_image, false
    end

    if ::File.readable? cover_image
      go_to_page page_count if face == :back
      if cover_image.downcase.end_with? '.pdf'
        # NOTE import_page automatically advances to next page afterwards (can we change this behavior?)
        import_page cover_image, advance: face != :back
      else
        image_page cover_image, canvas: true
      end
    else
      logger.warn %(#{face} cover image not found or readable: #{cover_image})
    end
  end
ensure
  unlink_tmp_file cover_image if cover_image
end
layout_footnotes(node) click to toggle source

QUESTION if a footnote ref appears in a separate chapter, should the footnote def be duplicated?

# File lib/asciidoctor-pdf/converter.rb, line 504
def layout_footnotes node
  return if (fns = (doc = node.document).footnotes - @footnotes).empty?
  theme_margin :footnotes, :top
  theme_font :footnotes do
    # FIXME layout_caption resets the theme font for footnotes
    (title = doc.attr 'footnotes-title') && (layout_caption title)
    item_spacing = @theme.footnotes_item_spacing || 0
    fns.each do |fn|
      layout_prose %(<a name="_footnotedef_#{index = fn.index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{index}</a>] #{fn.text}), margin_bottom: item_spacing
    end
    @footnotes += fns
  end
  nil
end
layout_heading(string, opts = {}) click to toggle source

QUESTION why doesn't layout_heading set the font?? QUESTION why doesn't layout_heading accept a node?

# File lib/asciidoctor-pdf/converter.rb, line 2492
def layout_heading string, opts = {}
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme[%(heading_h#{opts[:level]}_margin_top)] || @theme.heading_margin_top
  bot_margin = margin || (opts.delete :margin_bottom) || @theme[%(heading_h#{opts[:level]}_margin_bottom)] || @theme.heading_margin_bottom
  if (transform = (opts.delete :text_transform) || @text_transform) && transform != 'none'
    string = transform_text string, transform
  end
  margin_top top_margin
  typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme[%(heading_h#{opts[:level]}_line_height)] || @theme.heading_line_height), {
    color: @font_color,
    inline_format: true,
    align: @base_align.to_sym
  }.merge(opts)
  margin_bottom bot_margin
end
layout_part_title(node, title, opts = {})
layout_prose(string, opts = {}) click to toggle source

NOTE inline_format is true by default

# File lib/asciidoctor-pdf/converter.rb, line 2508
def layout_prose string, opts = {}
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top
  bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
  if (transform = (opts.delete :text_transform) || @text_transform) && transform != 'none'
    string = transform_text string, transform
  end
  # NOTE used by extensions; ensures linked text gets formatted using the link styles
  if (anchor = opts.delete :anchor)
    string = %(<a anchor="#{anchor}">#{string}</a>)
  end
  margin_top top_margin
  typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.base_line_height), {
    color: @font_color,
    # NOTE normalize makes endlines soft (replaces "\n" with ' ')
    inline_format: [normalize: (opts.delete :normalize) != false],
    align: @base_align.to_sym
  }.merge(opts)
  margin_bottom bot_margin
end
layout_running_content(periphery, doc, opts = {}) click to toggle source

TODO delegate to layout_page_header and layout_page_footer per page

# File lib/asciidoctor-pdf/converter.rb, line 2718
def layout_running_content periphery, doc, opts = {}
  skip, skip_pagenums, body_start_page_number = opts[:skip] || [1, 1]
  body_start_page_number = opts[:body_start_page_number] || 1
  # NOTE find and advance to first non-imported content page to use as model page
  return unless (content_start_page = state.pages[skip..-1].index {|p| !p.imported_page? })
  content_start_page += (skip + 1)
  num_pages = page_count - skip
  prev_page_number = page_number
  go_to_page content_start_page

  # FIXME probably need to treat doctypes differently
  is_book = doc.doctype == 'book'
  header = doc.header? ? doc.header : nil
  # TODO make this section threshold configurable (perhaps in theme?)
  sections = doc.find_by(context: :section) {|sect| sect.level < 3 && sect != header } || []

  # FIXME we need a proper model for all this page counting
  # FIXME we make a big assumption that part & chapter start on new pages
  # index parts, chapters and sections by the visual page number on which they start
  part_start_pages = {}
  chapter_start_pages = {}
  section_start_pages = {}
  trailing_section_start_pages = {}
  sections.each do |sect|
    page_num = (sect.attr 'pdf-page-start').to_i - skip_pagenums
    if is_book && ((sect_is_part = sect.part?) || sect.chapter?)
      if sect_is_part
        part_start_pages[page_num] ||= (sect.numbered_title formal: true)
      else
        chapter_start_pages[page_num] ||= (sect.numbered_title formal: true)
        if sect.sectname == 'appendix' && !part_start_pages.empty?
          # FIXME need a better way to indicate that part has ended
          part_start_pages[page_num] = ''
        end
      end
    else
      sect_title = trailing_section_start_pages[page_num] = sect.numbered_title formal: true
      section_start_pages[page_num] ||= sect_title
    end
  end

  # index parts, chapters, and sections by the visual page number on which they appear
  parts_by_page = {}
  chapters_by_page = {}
  sections_by_page = {}
  # QUESTION should the default part be the doctitle?
  last_part = nil
  # QUESTION should we enforce that the preamble is a preface?
  last_chap = is_book ? :pre : nil
  last_sect = nil
  sect_search_threshold = 1
  (1..num_pages).each do |num|
    if (part = part_start_pages[num])
      last_part = part
      last_chap = nil
      last_sect = nil
    end
    if (chap = chapter_start_pages[num])
      last_chap = chap
      last_sect = nil
    end
    if (sect = section_start_pages[num])
      last_sect = sect
    elsif part || chap
      sect_search_threshold = num
    # NOTE we didn't find a section on this page; look back to find last section started
    elsif last_sect
      ((sect_search_threshold)..(num - 1)).reverse_each do |prev|
        if (sect = trailing_section_start_pages[prev])
          last_sect = sect
          break
        end
      end
    end
    parts_by_page[num] = last_part
    if last_chap == :pre
      if num == 1
        chapters_by_page[num] = doc.doctitle
      elsif num >= body_start_page_number
        chapters_by_page[num] = is_book ? (doc.attr 'preface-title', 'Preface') : nil
      else
        chapters_by_page[num] = doc.attr 'toc-title'
      end
    else
      chapters_by_page[num] = last_chap
    end
    sections_by_page[num] = last_sect
  end

  doctitle = doc.doctitle partition: true, use_fallback: true
  # NOTE set doctitle again so it's properly escaped
  doc.set_attr 'doctitle', doctitle.combined
  doc.set_attr 'document-title', doctitle.main
  doc.set_attr 'document-subtitle', doctitle.subtitle
  doc.set_attr 'page-count', num_pages
  allow_uri_read = doc.attr? 'allow-uri-read'
  svg_fallback_font = default_svg_font

  if periphery == :header
    trim_line_metrics = calc_line_metrics(@theme.header_line_height || @theme.base_line_height)
    trim_top = page_height
    # NOTE height is required atm
    trim_height = @theme.header_height || page_margin_top
    trim_padding = inflate_padding @theme.header_padding || 0
    trim_bg_color = resolve_theme_color :header_background_color
    trim_border_width = @theme.header_border_width || @theme.base_border_width
    trim_border_style = (@theme.header_border_style || :solid).to_sym
    trim_border_color = resolve_theme_color :header_border_color
    trim_valign = (@theme.header_vertical_align || :middle).to_sym
    trim_img_valign = @theme.header_image_vertical_align
  else
    trim_line_metrics = calc_line_metrics(@theme.footer_line_height || @theme.base_line_height)
    # NOTE height is required atm
    trim_top = trim_height = @theme.footer_height || page_margin_bottom
    trim_padding = inflate_padding @theme.footer_padding || 0
    trim_bg_color = resolve_theme_color :footer_background_color
    trim_border_width = @theme.footer_border_width || @theme.base_border_width
    trim_border_style = (@theme.footer_border_style || :solid).to_sym
    trim_border_color = resolve_theme_color :footer_border_color
    trim_valign = (@theme.footer_vertical_align || :middle).to_sym
    trim_img_valign = @theme.footer_image_vertical_align
  end

  trim_stamp_name = {
    recto: %(#{periphery}_recto),
    verso: %(#{periphery}_verso)
  }
  trim_left = {
    recto: @page_margin_by_side[:recto][3],
    verso: @page_margin_by_side[:verso][3]
  }
  trim_width = {
    recto: page_width - trim_left[:recto] - @page_margin_by_side[:recto][1],
    verso: page_width - trim_left[:verso] - @page_margin_by_side[:verso][1]
  }
  trim_content_left = {
    recto: trim_left[:recto] + trim_padding[3],
    verso: trim_left[:verso] + trim_padding[3]
  }
  trim_content_width = {
    recto: trim_width[:recto] - trim_padding[3] - trim_padding[1],
    verso: trim_width[:verso] - trim_padding[3] - trim_padding[1]
  }
  trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom
  trim_border_color = nil if trim_border_width == 0
  trim_valign = :center if trim_valign == :middle
  case trim_img_valign
  when nil
    trim_img_valign = trim_valign
  when 'middle'
    trim_img_valign = :center
  when 'top', 'center', 'bottom'
    trim_img_valign = trim_img_valign.to_sym
  end

  colspec_dict = PageSides.inject({}) do |acc, side|
    side_trim_content_width = trim_content_width[side]
    if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)] || @theme[%(#{periphery}_columns)])
      case (colspecs = (custom_colspecs.to_s.tr ',', ' ').split[0..2]).size
      when 3
        colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] }
      when 2
        colspecs = { left: colspecs[0], center: '0', right: colspecs[1] }
      when 0, 1
        colspecs = { left: '0', center: colspecs[0] || '100', right: '0' }
      end
      tot_width = 0
      side_colspecs = colspecs.map {|col, spec|
        if (alignment_char = spec.chr).to_i.to_s != alignment_char
          alignment = AlignmentTable[alignment_char] || :left
          rel_width = spec[1..-1].to_f
        else
          alignment = :left
          rel_width = spec.to_f
        end
        tot_width += rel_width
        [col, { align: alignment, width: rel_width, x: 0 }]
      }.to_h
      # QUESTION should we allow the columns to overlap (capping width at 100%)?
      side_colspecs.each {|_, colspec| colspec[:width] = (colspec[:width] / tot_width) * side_trim_content_width }
      side_colspecs[:right][:x] = (side_colspecs[:center][:x] = side_colspecs[:left][:width]) + side_colspecs[:center][:width]
      acc[side] = side_colspecs
    else
      acc[side] = {
        left: { align: :left, width: side_trim_content_width, x: 0 },
        center: { align: :center, width: side_trim_content_width, x: 0 },
        right: { align: :right, width: side_trim_content_width, x: 0 }
      }
    end
    acc
  end

  # TODO move this to a method so it can be reused; cache results
  content_dict = PageSides.inject({}) do |acc, side|
    side_content = {}
    ColumnPositions.each do |position|
      unless (val = @theme[%(#{periphery}_#{side}_#{position}_content)]).nil_or_empty?
        # TODO support image URL (using resolve_image_path)
        if (val.include? ':') && val =~ ImageAttributeValueRx
          if ::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir')))
            attrs = (AttributeList.new $2).parse
            col_width = colspec_dict[side][position][:width]
            if (fit = attrs['fit']) == 'contain'
              width = col_width
            else
              unless (width = resolve_explicit_width attrs, col_width)
                # QUESTION should we lookup and scale intrinsic width if explicit width is not given?
                # NOTE failure message will be reported later when image is rendered
                width = (to_pt intrinsic_image_dimensions(path)[:width], :px) rescue 0
              end
              width = col_width if fit == 'scale-down' && width > col_width
            end
            side_content[position] = { path: path, width: width, fit: !!fit }
          else
            logger.warn %(image to embed not found or not readable: #{path})
            side_content[position] = val
          end
        else
          side_content[position] = val
        end
      end
    end
    # NOTE set fallbacks if not explicitly disabled
    if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none'
      side_content = { side == :recto ? :right : :left => '{page-number}' }
    end

    acc[side] = side_content
    acc
  end

  stamps = {}
  if trim_bg_color || trim_border_color
    PageSides.each do |side|
      create_stamp trim_stamp_name[side] do
        canvas do
          if trim_bg_color
            bounding_box [0, trim_top], width: bounds.width, height: trim_height do
              fill_bounds trim_bg_color
              if trim_border_color
                # TODO stroke_horizontal_rule should support :at
                move_down bounds.height if periphery == :header
                stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
              end
            end
          else
            bounding_box [trim_left[side], trim_top], width: trim_width[side], height: trim_height do
              # TODO stroke_horizontal_rule should support :at
              move_down bounds.height if periphery == :header
              stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
            end
          end
        end
      end
    end
    stamps[periphery] = true
  end

  pagenums_enabled = doc.attr? 'pagenums'
  attribute_missing_doc = doc.attr 'attribute-missing'
  case @media == 'prepress' ? 'physical' : (doc.attr 'pdf-folio-placement')
  when 'physical'
    folio_basis, invert_folio = :physical, false
  when 'physical-inverted'
    folio_basis, invert_folio = :physical, true
  when 'virtual-inverted'
    folio_basis, invert_folio = :virtual, true
  else
    folio_basis, invert_folio = :virtual, false
  end
  repeat((content_start_page..page_count), dynamic: true) do
    # NOTE don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
    next if page.imported_page?
    pgnum_label = page_number - skip_pagenums
    pgnum_label = (RomanNumeral.new page_number, :lower) if pgnum_label < 1
    side = page_side((folio_basis == :physical ? page_number : pgnum_label), invert_folio)
    # FIXME we need to have a content setting for chapter pages
    content_by_position, colspec_by_position = content_dict[side], colspec_dict[side]
    # TODO populate chapter-number
    # TODO populate numbered and unnumbered chapter and section titles
    doc.set_attr 'page-number', pgnum_label.to_s if pagenums_enabled
    # QUESTION should the fallback value be nil instead of empty string? or should we remove attribute if no value?
    doc.set_attr 'part-title', (parts_by_page[pgnum_label] || '')
    doc.set_attr 'chapter-title', (chapters_by_page[pgnum_label] || '')
    doc.set_attr 'section-title', (sections_by_page[pgnum_label] || '')
    doc.set_attr 'section-or-chapter-title', (sections_by_page[pgnum_label] || chapters_by_page[pgnum_label] || '')

    stamp trim_stamp_name[side] if stamps[periphery]

    theme_font periphery do
      canvas do
        bounding_box [trim_content_left[side], trim_top], width: trim_content_width[side], height: trim_height do
          ColumnPositions.each do |position|
            next unless (content = content_by_position[position])
            next unless (colspec = colspec_by_position[position])[:width] > 0
            # FIXME we need to have a content setting for chapter pages
            case content
            when ::Hash
              # NOTE image vposition respects padding; use negative image_vertical_align value to revert
              trim_v_padding = trim_padding[0] + trim_padding[2]
              # NOTE float ensures cursor position is restored and returns us to current page if we overrun
              float do
                # NOTE bounding_box is redundant if trim_v_padding is 0
                bounding_box [colspec[:x], cursor - trim_padding[0]], width: colspec[:width], height: (bounds.height - trim_v_padding) do
                  begin
                    if (img_path = content[:path]).downcase.end_with? '.svg'
                      svg_data = ::File.read img_path
                      svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
                          position: colspec[:align],
                          vposition: trim_img_valign,
                          width: content[:width],
                          # TODO enforce jail in safe mode
                          enable_file_requests_with_root: (::File.dirname img_path),
                          enable_web_requests: allow_uri_read,
                          fallback_font_name: svg_fallback_font
                      if content[:fit] && svg_obj.document.sizing.output_height > (available_h = bounds.height)
                        svg_obj.resize height: available_h
                      end
                      svg_obj.draw
                    else
                      img_opts = { position: colspec[:align], vposition: trim_img_valign }
                      if content[:fit]
                        img_opts[:fit] = [content[:width], bounds.height]
                      else
                        img_opts[:width] = content[:width]
                      end
                      image img_path, img_opts
                    end
                  rescue
                    logger.warn %(could not embed image in running content: #{img_path}; #{$!.message})
                  end
                end
              end
            when ::String
              # NOTE minor optimization
              if content == '{page-number}'
                content = pagenums_enabled ? pgnum_label.to_s : nil
              else
                # FIXME get apply_subs to handle drop-line w/o a warning
                doc.set_attr 'attribute-missing', 'skip' unless attribute_missing_doc == 'skip'
                if (content = doc.apply_subs content).include? '{'
                  # NOTE must use &#123; in place of {, not \{, to escape attribute reference
                  content = content.split(LF).delete_if {|line| SimpleAttributeRefRx.match? line } * LF
                end
                doc.set_attr 'attribute-missing', attribute_missing_doc unless attribute_missing_doc == 'skip'
              end
              theme_font %(#{periphery}_#{side}_#{position}) do
                formatted_text_box parse_text(content, color: @font_color, inline_format: [normalize: true]),
                  at: [colspec[:x], trim_top - trim_padding[0] + (trim_valign == :center ? font.descender * 0.5 : 0)],
                  width: colspec[:width],
                  height: trim_content_height,
                  align: colspec[:align],
                  valign: trim_valign,
                  leading: trim_line_metrics.leading,
                  final_gap: false,
                  overflow: :truncate
              end
            end
          end
        end
      end
    end
  end

  go_to_page prev_page_number
  nil
end
layout_table_caption(node, width, alignment = :left, side = :top) click to toggle source

Render the caption for a table and return the height of the rendered content

# File lib/asciidoctor-pdf/converter.rb, line 2584
def layout_table_caption node, width, alignment = :left, side = :top
  # QUESTION should we confine width of title to width of table?
  if alignment == :left || (excess = bounds.width - width) == 0
    layout_caption node, side: side
  else
    indent excess * (alignment == :center ? 0.5 : 1) do
      layout_caption node, side: side
    end
  end
end
layout_title_page(doc) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2328
def layout_title_page doc
  return unless doc.header? && !doc.notitle

  prev_bg_image = @page_bg_image
  prev_bg_color = @page_bg_color

  if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
    @page_bg_image = (bg_image == 'none' ? nil : bg_image)
  end
  if (bg_color = resolve_theme_color :title_page_background_color)
    @page_bg_color = bg_color
  end
  # NOTE a new page will already be started if the cover image is a PDF
  start_new_page unless page_is_empty?
  start_new_page if @ppbook && verso_page?
  @page_bg_image = prev_bg_image if bg_image
  @page_bg_color = prev_bg_color if bg_color

  # IMPORTANT this is the first page created, so we need to set the base font
  font @theme.base_font_family, size: @theme.base_font_size

  # QUESTION allow alignment per element on title page?
  title_align = (@theme.title_page_align || @base_align).to_sym

  # TODO disallow .pdf as image type
  if (logo_image_path = (doc.attr 'title-logo-image', @theme.title_page_logo_image))
    if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
      logo_image_path = $1
      logo_image_attrs = (AttributeList.new $2).parse ['alt', 'width', 'height']
      relative_to_imagesdir = true
    else
      logo_image_attrs = {}
      relative_to_imagesdir = false
    end
    # HACK quick fix to resolve image path relative to theme
    logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, (doc.attr 'pdf-stylesdir') unless doc.attr? 'title-logo-image'
    logo_image_attrs['target'] = logo_image_path
    logo_image_attrs['align'] ||= (@theme.title_page_logo_align || title_align.to_s)
    # QUESTION should we allow theme to turn logo image off?
    logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top || '10%'
    # FIXME delegate to method to convert page % to y value
    if logo_image_top.end_with? 'vh'
      logo_image_top = page_height - page_height * logo_image_top.to_f / 100.0
    else
      logo_image_top = bounds.absolute_top - effective_page_height * logo_image_top.to_f / 100.0
    end
    initial_y, @y = @y, logo_image_top
    # FIXME add API to Asciidoctor for creating blocks like this (extract from extensions module?)
    image_block = ::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs
    # NOTE pinned option keeps image on same page
    indent (@theme.title_page_logo_margin_left || 0), (@theme.title_page_logo_margin_right || 0) do
      convert_image image_block, relative_to_imagesdir: relative_to_imagesdir, pinned: true
    end
    @y = initial_y
  end

  # TODO prevent content from spilling to next page
  theme_font :title_page do
    doctitle = doc.doctitle partition: true
    if (title_top = @theme.title_page_title_top)
      if title_top.end_with? 'vh'
        title_top = page_height - page_height * title_top.to_f / 100.0
      else
        title_top = bounds.absolute_top - effective_page_height * title_top.to_f / 100.0
      end
      # FIXME delegate to method to convert page % to y value
      @y = title_top
    end
    move_down(@theme.title_page_title_margin_top || 0)
    indent (@theme.title_page_title_margin_left || 0), (@theme.title_page_title_margin_right || 0) do
      theme_font :title_page_title do
        layout_heading doctitle.main,
          align: title_align,
          margin: 0,
          line_height: @theme.title_page_title_line_height
      end
    end
    move_down(@theme.title_page_title_margin_bottom || 0)
    if doctitle.subtitle
      move_down(@theme.title_page_subtitle_margin_top || 0)
      indent (@theme.title_page_subtitle_margin_left || 0), (@theme.title_page_subtitle_margin_right || 0) do
        theme_font :title_page_subtitle do
          layout_heading doctitle.subtitle,
            align: title_align,
            margin: 0,
            line_height: @theme.title_page_subtitle_line_height
        end
      end
      move_down(@theme.title_page_subtitle_margin_bottom || 0)
    end
    if doc.attr? 'authors'
      move_down(@theme.title_page_authors_margin_top || 0)
      indent (@theme.title_page_authors_margin_left || 0), (@theme.title_page_authors_margin_right || 0) do
        # TODO provide an API in core to get authors as an array
        authors = (1..(doc.attr 'authorcount', 1).to_i).map {|idx|
          doc.attr(idx == 1 ? 'author' : %(author_#{idx}))
        } * (@theme.title_page_authors_delimiter || ', ')
        theme_font :title_page_authors do
          layout_prose authors,
            align: title_align,
            margin: 0,
            normalize: false
        end
      end
      move_down(@theme.title_page_authors_margin_bottom || 0)
    end
    revision_info = [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact
    unless revision_info.empty?
      move_down(@theme.title_page_revision_margin_top || 0)
      revision_text = revision_info * (@theme.title_page_revision_delimiter || ', ')
      indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
        theme_font :title_page_revision do
          layout_prose revision_text,
            align: title_align,
            margin: 0,
            normalize: false
        end
      end
      move_down(@theme.title_page_revision_margin_bottom || 0)
    end
  end
end
layout_toc(doc, num_levels = 2, toc_page_number = 2, num_front_matter_pages = 0, start_at = nil) click to toggle source

NOTE num_front_matter_pages is not used during a dry run

# File lib/asciidoctor-pdf/converter.rb, line 2596
def layout_toc doc, num_levels = 2, toc_page_number = 2, num_front_matter_pages = 0, start_at = nil
  go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
  start_page_number = page_number
  @y = start_at if start_at
  theme_font :heading, level: 2 do
    theme_font :toc_title do
      toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
      layout_heading((doc.attr 'toc-title'), align: toc_title_align)
    end
  end
  # QUESTION should we skip this whole method if num_levels < 0?
  unless num_levels < 0
    dot_leader = theme_font :toc do
      # TODO we could simplify by using nested theme_font :toc_dot_leader
      if (dot_leader_font_style = (@theme.toc_dot_leader_font_style || :normal).to_sym) != font_style
        font_style dot_leader_font_style
      end
      {
        font_color: @theme.toc_dot_leader_font_color || @font_color,
        font_style: dot_leader_font_style,
        levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new :
            (dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (0..num_levels).to_set)),
        text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault),
        width: dot_leader_text.empty? ? 0 : (rendered_width_of_string dot_leader_text),
        # TODO spacer gives a little bit of room between dots and page number
        spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) },
        spacer_width: (rendered_width_of_char NoBreakSpace, size: spacer_font_size)
      }
    end
    line_metrics = calc_line_metrics @theme.toc_line_height
    theme_margin :toc, :top
    layout_toc_level doc.sections, num_levels, line_metrics, dot_leader, num_front_matter_pages
  end
  # NOTE range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
  toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
  go_to_page page_count - 1 unless scratch?
  toc_page_numbers
end
layout_toc_level(sections, num_levels, line_metrics, dot_leader, num_front_matter_pages = 0) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2635
def layout_toc_level sections, num_levels, line_metrics, dot_leader, num_front_matter_pages = 0
  # NOTE font options aren't always reliable, so store size separately
  toc_font_info = theme_font :toc do
    { font: font, size: @font_size }
  end
  sections.each do |sect|
    theme_font :toc, level: (sect.level + 1) do
      sect_title = (transform = @text_transform) && transform != 'none' ?
          (transform_text sect.numbered_title, transform) : sect.numbered_title
      # NOTE only write section title (excluding dots and page number) if this is a dry run
      if scratch?
        # FIXME use layout_prose
        # NOTE must wrap title in empty anchor element in case links are styled with different font family / size
        typeset_text %(<a>#{sect_title}</a>), line_metrics, inline_format: true
      else
        pgnum_label = ((sect.attr 'pdf-page-start') - num_front_matter_pages).to_s
        start_page_number = page_number
        start_cursor = cursor
        # NOTE use low-level text formatter to add anchor overlay without styling text as link & force color
        sect_title_format_override = {
          anchor: (sect_anchor = sect.attr 'pdf-anchor'),
          color: @font_color,
          styles: ((@theme[%(toc_h#{sect.level + 1}_text_decoration)] || @theme.toc_text_decoration) == 'underline' ?
              (font_styles << :underline) : font_styles)
        }
        (sect_title_fragments = text_formatter.format sect_title).each do |fragment|
          fragment.update sect_title_format_override do |key, old_val, new_val|
            key == :styles ? (old_val.merge new_val) : new_val
          end
        end
        pgnum_label_width = rendered_width_of_string pgnum_label
        indent 0, pgnum_label_width do
          typeset_formatted_text sect_title_fragments, line_metrics
        end
        end_page_number = page_number
        end_cursor = cursor
        # TODO it would be convenient to have a cursor mark / placement utility that took page number into account
        go_to_page start_page_number if start_page_number != end_page_number
        move_cursor_to start_cursor
        if dot_leader[:width] > 0 && (dot_leader[:levels].include? sect.level)
          pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
          # WARNING width_of is not accurate if string must use characters from fallback font
          sect_title_width = width_of sect_title, inline_format: true
          save_font do
            # NOTE the same font is used for dot leaders throughout toc
            set_font toc_font_info[:font], toc_font_info[:size]
            font_style dot_leader[:font_style]
            num_dots = ((bounds.width - sect_title_width - dot_leader[:spacer_width] - pgnum_label_width) / dot_leader[:width]).floor
            # FIXME dots don't line up in columns if width of page numbers differ
            typeset_formatted_text [
                { text: (dot_leader[:text] * (num_dots < 0 ? 0 : num_dots)), color: dot_leader[:font_color] },
                dot_leader[:spacer],
                { text: pgnum_label, anchor: sect_anchor }.merge(pgnum_label_font_settings)
              ], line_metrics, align: :right
          end
        else
          typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: sect_anchor }], line_metrics, align: :right
        end
        go_to_page end_page_number if page_number != end_page_number
        move_cursor_to end_cursor
      end
    end
    indent @theme.toc_indent do
      layout_toc_level sect.sections, num_levels, line_metrics, dot_leader, num_front_matter_pages
    end if sect.level < num_levels
  end
end
load_theme(doc) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 347
def load_theme doc
  @theme ||= doc.options[:pdf_theme] || ThemeLoader.load_theme((doc.attr 'pdf-style'), (doc.attr 'pdf-stylesdir'))
end
margin(n, side) click to toggle source

Insert a margin space at the specified side unless cursor is at the top of the page. Start a new page if n value is greater than remaining space on page.

# File lib/asciidoctor-pdf/converter.rb, line 3194
def margin n, side
  unless n == 0 || at_page_top?
    # NOTE use low-level cursor calculation to workaround cursor bug in column_box context
    if y - reference_bounds.absolute_bottom > n
      move_down n
    else
      # set cursor at top of next page
      reference_bounds.move_past_bottom
    end
  end
end
margin_bottom(n) click to toggle source

Insert a bottom margin space unless cursor is at the top of the page (not likely). Start a new page if n value is greater than remaining space on page.

# File lib/asciidoctor-pdf/converter.rb, line 3188
def margin_bottom n
  margin n, :bottom
end
margin_top(n) click to toggle source

Insert a top margin space unless cursor is at the top of the page. Start a new page if n value is greater than remaining space on page.

# File lib/asciidoctor-pdf/converter.rb, line 3182
def margin_top n
  margin n, :top
end
on_image_error(reason, node, target, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 1365
def on_image_error reason, node, target, opts = {}
  logger.warn opts[:message] if opts.key? :message
  alt_text = (link = node.attr 'link', nil, false) ?
      %(<a href="#{link}">[#{node.attr 'alt'}]</a> | <em>#{target}</em>) :
      %([#{node.attr 'alt'}] | <em>#{target}</em>)
  layout_prose alt_text,
      align: ((node.attr 'align', nil, false) || @theme.image_align).to_sym,
      margin: 0,
      normalize: false,
      single_line: true
  layout_caption node, side: :bottom if node.title?
  theme_margin :block, :bottom unless opts[:pinned]
  nil
end
pop_scratch(doc) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3675
def pop_scratch doc
  if (@scratch_depth -= 1) == 0
    doc.catalog.replace @save_state[:catalog]
    doc.attributes.replace @save_state[:attributes]
    @save_state = nil
  end
end
preresolve_explicit_width(attrs) click to toggle source

Resolves the explicit width as a PDF pt value if the value is specified in absolute units, but defers resolving a percentage value until later.

See resolve_explicit_width method for details about which attributes are considered.

# File lib/asciidoctor-pdf/converter.rb, line 3564
def preresolve_explicit_width attrs
  if attrs.key? 'pdfwidth'
    ((width = attrs['pdfwidth']).end_with? '%') ? width : (str_to_pt width)
  elsif attrs.key? 'scaledwidth'
    # NOTE the parser automatically appends % if value is unitless
    ((width = attrs['scaledwidth']).end_with? '%') ? width : (str_to_pt width)
  elsif attrs.key? 'width'
    # QUESTION should we honor percentage width value?
    to_pt attrs['width'].to_f, :px
  end
end
preserve_indentation(string, tab_size = nil, guard_indent = true) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3365
def preserve_indentation string, tab_size = nil, guard_indent = true
  return '' unless string
  # expand tabs if they aren't already expanded, even if explicitly disabled
  # NOTE Asciidoctor >= 1.5.3 already replaces tabs if tabsize attribute is positive
  if ((tab_size = tab_size.to_i) < 1 || !@capabilities[:expands_tabs]) && (string.include? TAB)
    # Asciidoctor <= 1.5.2 already does tab replacement in some cases, so be consistent about tab size
    full_tab_space = ' ' * (tab_size = 4)
    result = []
    string.each_line do |line|
      if line.start_with? TAB
        if guard_indent
          # NOTE '+' operator is faster than interpolation
          line.sub!(TabIndentRx) { GuardedIndent + (full_tab_space * $&.length).chop! }
        else
          line.sub!(TabIndentRx) { full_tab_space * $&.length }
        end
        leading_space = false
      # QUESTION should we check for LF first?
      elsif line == LF
        result << line
        next
      else
        leading_space = guard_indent && (line.start_with? ' ')
      end

      if line.include? TAB
        # keep track of how many spaces were added to adjust offset in match data
        spaces_added = 0
        line.gsub!(TabRx) {
          # calculate how many spaces this tab represents, then replace tab with spaces
          if (offset = ($~.begin 0) + spaces_added) % tab_size == 0
            spaces_added += (tab_size - 1)
            full_tab_space
          else
            unless (spaces = tab_size - offset % tab_size) == 1
              spaces_added += (spaces - 1)
            end
            ' ' * spaces
          end
        }
      end

      # NOTE we save time by adding indent guard per line while performing tab expansion
      line[0] = GuardedIndent if leading_space
      result << line
    end
    result.join
  else
    if guard_indent
      string[0] = GuardedIndent if string.start_with? ' '
      string.gsub! InnerIndent, GuardedInnerIndent if string.include? InnerIndent
    end
    string
  end
end
push_scratch(doc) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3666
def push_scratch doc
  if (@scratch_depth += 1) == 1
    @save_state = {
      catalog: {}.tap {|accum| doc.catalog.each {|k, v| accum[k] = v.dup } },
      attributes: doc.attributes.dup,
    }
  end
end
register_fonts(font_catalog, scripts = 'latin', fonts_dir) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3145
def register_fonts font_catalog, scripts = 'latin', fonts_dir
  (font_catalog || {}).each do |key, styles|
    register_font key => styles.map {|style, path| [style.to_sym, (font_path path, fonts_dir)]}.to_h
  end

  # FIXME read kerning setting from theme!
  default_kerning true
end
rendered_width_of_char(char, opts = {}) click to toggle source

Compute the rendered width of a char, taking fallback fonts into account

# File lib/asciidoctor-pdf/converter.rb, line 3327
def rendered_width_of_char char, opts = {}
  if @fallback_fonts.empty? || (font.glyph_present? char)
    width_of_string char, opts
  else
    @fallback_fonts.each do |fallback_font|
      font fallback_font do
        return width_of_string char, opts if font.glyph_present? char
      end
    end
    width_of_string char, opts
  end
end
rendered_width_of_string(str, opts = {}) click to toggle source

Compute the rendered width of a string, taking fallback fonts into account

# File lib/asciidoctor-pdf/converter.rb, line 3315
def rendered_width_of_string str, opts = {}
  if str.length == 1
    rendered_width_of_char str, opts
  elsif (chars = str.each_char).all? {|char| font.glyph_present? char }
    width_of_string str, opts
  else
    char_widths = chars.map {|char| rendered_width_of_char char, opts }
    char_widths.reduce(&:+) + (char_widths.length * character_spacing)
  end
end
resolve_alignment_from_role(roles) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3455
def resolve_alignment_from_role roles
  if (align_role = roles.reverse.find {|r| TextAlignmentRoles.include? r })
    align_role[5..-1].to_sym
  else
    nil
  end
end
resolve_background_image(doc, theme, key) click to toggle source

Resolve the path to the background image either from a document attribute or theme key.

Returns The string “none” if the background image value is none, otherwise the resolved path to the image. If neither the document attribute or theme key are specified, or the image path cannot be resolved, return nil.

# File lib/asciidoctor-pdf/converter.rb, line 3535
def resolve_background_image doc, theme, key
  if (bg_image = (doc_attr_val = (doc.attr key)) || theme[(key.tr '-', '_').to_sym])
    return bg_image if bg_image == 'none'

    if (bg_image.include? ':') && bg_image =~ ImageAttributeValueRx
      # QUESTION should we support width and height in this case?
      # TODO support explicit format
      bg_image = $1
      relative_to_imagesdir = true
    else
      relative_to_imagesdir = false
    end

    if (bg_image = doc_attr_val ? (resolve_image_path doc, bg_image, relative_to_imagesdir) :
        (ThemeLoader.resolve_theme_asset bg_image, (doc.attr 'pdf-stylesdir')))
      if ::File.readable? bg_image
        bg_image
      else
        logger.warn %(#{key.tr '-', ' '} not found or readable: #{bg_image})
        nil
      end
    end
  end
end
resolve_explicit_width(attrs, max_width = bounds.width, opts = {}) click to toggle source

Resolves the explicit width as a PDF pt value, if specified.

Resolves the explicit width, first considering the pdfwidth attribute, then the scaledwidth attribute and finally the width attribute. If the specified value is in pixels, the value is scaled by 75% to perform approximate CSS px to PDF pt conversion. If the resolved width is larger than the max_width, the max_width value is returned.

# File lib/asciidoctor-pdf/converter.rb, line 3585
def resolve_explicit_width attrs, max_width = bounds.width, opts = {}
  # QUESTION should we restrict width to max_width for pdfwidth?
  if attrs.key? 'pdfwidth'
    if (width = attrs['pdfwidth']).end_with? '%'
      (width.to_f / 100) * max_width
    elsif opts[:support_vw] && (width.end_with? 'vw')
      (width.chomp 'vw').extend ViewportWidth
    else
      str_to_pt width
    end
  elsif attrs.key? 'scaledwidth'
    # NOTE the parser automatically appends % if value is unitless
    if (width = attrs['scaledwidth']).end_with? '%'
      (width.to_f / 100) * max_width
    else
      str_to_pt width
    end
  elsif opts[:use_fallback] && (width = @theme.image_width)
    if width.end_with? '%'
      (width.to_f / 100) * max_width
    elsif opts[:support_vw] && (width.end_with? 'vw')
      (width.chomp 'vw').extend ViewportWidth
    else
      str_to_pt width
    end
  elsif attrs.key? 'width'
    # QUESTION should we honor percentage width value?
    [max_width, (to_pt attrs['width'].to_f, :px)].min
  end
end
resolve_image_path(node, image_path = nil, relative_to_imagesdir = true, image_format = nil) click to toggle source

Resolve the system path of the specified image path.

Resolve and normalize the absolute system path of the specified image, taking into account the imagesdir attribute. If an image path is not specified, the path is read from the target attribute of the specified document node.

If the target is a URI and the allow-uri-read attribute is set on the document, read the file contents to a temporary file and return the path to the temporary file. If the target is a URI and the allow-uri-read attribute is not set, or the URI cannot be read, this method returns a nil value.

When a temporary file is used, the TemporaryPath type is mixed into the path string.

# File lib/asciidoctor-pdf/converter.rb, line 3485
def resolve_image_path node, image_path = nil, relative_to_imagesdir = true, image_format = nil
  doc = node.document
  imagesdir = relative_to_imagesdir ? (resolve_imagesdir doc) : nil
  image_path ||= node.attr 'target'
  image_format ||= ::Asciidoctor::Image.format image_path, (::Asciidoctor::Image === node ? node : nil)
  # NOTE currently used for inline images
  if ::Base64 === image_path
    tmp_image = ::Tempfile.create ['image-', image_format && %(.#{image_format})]
    tmp_image.binmode unless image_format == 'svg'
    begin
      tmp_image.write(::Base64.decode64 image_path)
      tmp_image.path.extend TemporaryPath
    rescue
      nil
    ensure
      tmp_image.close
    end
  # handle case when image is a URI
  elsif (node.is_uri? image_path) || (imagesdir && (node.is_uri? imagesdir) &&
      (image_path = (node.normalize_web_path image_path, imagesdir, false)))
    unless doc.attr? 'allow-uri-read'
      logger.warn %(allow-uri-read is not enabled; cannot embed remote image: #{image_path}) unless scratch?
      return
    end
    if doc.attr? 'cache-uri'
      Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
    else
      ::OpenURI
    end
    tmp_image = ::Tempfile.create ['image-', image_format && %(.#{image_format})]
    tmp_image.binmode if (binary = image_format != 'svg')
    begin
      open(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write fd.read }
      tmp_image.path.extend TemporaryPath
    rescue
      nil
    ensure
      tmp_image.close
    end
  # handle case when image is a local file
  else
    ::File.expand_path(node.normalize_system_path image_path, imagesdir, nil, target_name: 'image')
  end
end
resolve_imagesdir(doc) click to toggle source

QUESTION is this method still necessary?

# File lib/asciidoctor-pdf/converter.rb, line 3464
def resolve_imagesdir doc
  if (imagesdir = doc.attr 'imagesdir').nil_or_empty? || (imagesdir = imagesdir.chomp '/') == '.'
    nil
  else
    imagesdir
  end
end
resolve_theme_color(key, fallback_color = nil) click to toggle source

QUESTION should we pass a category as an argument? QUESTION should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)

# File lib/asciidoctor-pdf/converter.rb, line 3165
def resolve_theme_color key, fallback_color = nil
  if (color = @theme[key.to_s]) && color != 'transparent'
    color
  else
    fallback_color
  end
end
restore_conums(fragments, conum_mapping, num_trailing_spaces = 0, linenums = nil) click to toggle source

Restore the conums into the Array of formatted text fragments

# File lib/asciidoctor-pdf/converter.rb, line 1643
def restore_conums fragments, conum_mapping, num_trailing_spaces = 0, linenums = nil
  lines = []
  line_num = 0
  # reorganize the fragments into an array of lines
  fragments.each do |fragment|
    line = (lines[line_num] ||= [])
    if (text = fragment[:text]) == LF
      line_num += 1
    elsif text.include? LF
      text.split(LF, -1).each_with_index do |line_in_fragment, idx|
        line = (lines[line_num += 1] ||= []) unless idx == 0
        line << (fragment.merge text: line_in_fragment) unless line_in_fragment.empty?
      end
    else
      line << fragment
    end
  end
  conum_color = @theme.conum_font_color
  last_line_num = lines.size - 1
  if linenums
    pad_size = (last_line_num + 1).to_s.length
    linenum_color = @theme.code_linenum_font_color
  end
  # append conums to appropriate lines, then flatten to an array of fragments
  lines.flat_map.with_index do |line, cur_line_num|
    last_line = cur_line_num == last_line_num
    line.unshift text: %(#{(cur_line_num + linenums).to_s.rjust pad_size} ), color: linenum_color if linenums
    if (conums = conum_mapping.delete cur_line_num)
      line << { text: ' ' * num_trailing_spaces } if last_line && num_trailing_spaces > 0
      conum_text = conums.map {|num| conum_glyph num } * ' '
      line << (conum_color ? { text: conum_text, color: conum_color } : { text: conum_text })
    end
    line << { text: LF } unless last_line
    line
  end
end
start_new_chapter(chapter) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 2477
def start_new_chapter chapter
  start_new_page unless at_page_top?
  # TODO must call update_colors before advancing to next page if start_new_page is called in layout_chapter_title
  start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
end
Also aliased as: start_new_part
start_new_part(chapter)
Alias for: start_new_chapter
theme_fill_and_stroke_bounds(category, opts = {}) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3173
def theme_fill_and_stroke_bounds category, opts = {}
  background_color = opts[:background_color] || @theme[%(#{category}_background_color)]
  fill_and_stroke_bounds background_color, @theme[%(#{category}_border_color)],
      line_width: @theme[%(#{category}_border_width)],
      radius: @theme[%(#{category}_border_radius)]
end
theme_font(category, opts = {}) { || ... } click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3214
def theme_font category, opts = {}
  result = nil
  # TODO inheriting from generic category should be an option
  if opts.key? :level
    level = opts[:level]
    family = @theme[%(#{category}_h#{level}_font_family)] || @theme[%(#{category}_font_family)] || @theme.base_font_family
    size = @theme[%(#{category}_h#{level}_font_size)] || @theme[%(#{category}_font_size)] || @theme.base_font_size
    style = @theme[%(#{category}_h#{level}_font_style)] || @theme[%(#{category}_font_style)]
    color = @theme[%(#{category}_h#{level}_font_color)] || @theme[%(#{category}_font_color)]
    # NOTE global text_transform is not currently supported
    transform = @theme[%(#{category}_h#{level}_text_transform)] || @theme[%(#{category}_text_transform)]
  else
    inherited_font = font_info
    family = @theme[%(#{category}_font_family)] || inherited_font[:family]
    size = @theme[%(#{category}_font_size)] || inherited_font[:size]
    style = @theme[%(#{category}_font_style)] || inherited_font[:style]
    color = @theme[%(#{category}_font_color)]
    # NOTE global text_transform is not currently supported
    transform = @theme[%(#{category}_text_transform)]
  end

  prev_color, @font_color = @font_color, color if color
  prev_transform, @text_transform = @text_transform, transform if transform

  font family, size: size, style: (style && style.to_sym) do
    result = yield
  end

  @font_color = prev_color if color
  @text_transform = prev_transform if transform
  result
end
theme_font_size_autofit(fragments, category) click to toggle source

Calculate the font size (down to the minimum font size) that would allow all the specified fragments to fit in the available width without wrapping lines.

Return the calculated font size if an adjustment is necessary or nil if no font size adjustment is necessary.

# File lib/asciidoctor-pdf/converter.rb, line 3252
def theme_font_size_autofit fragments, category
  arranger = arrange_fragments_by_line fragments
  theme_font category do
    # NOTE finalizing the line here generates fragments & calculates their widths using the current font settings
    # CAUTION it also removes zero-width spaces
    arranger.finalize_line
    actual_width = width_of_fragments arranger.fragments
    unless ::Array === (padding = @theme[%(#{category}_padding)])
      padding = ::Array.new 4, padding
    end
    available_width = bounds.width - (padding[3] || 0) - (padding[1] || 0)
    if actual_width > available_width
      adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate 4
      if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
        min
      else
        adjusted_font_size
      end
    else
      nil
    end
  end
end
theme_margin(category, side) click to toggle source

Lookup margin for theme element and side, then delegate to margin method. If margin value is not found, assume:

  • 0 when side == :top

  • @theme.vertical_spacing when side == :bottom

# File lib/asciidoctor-pdf/converter.rb, line 3210
def theme_margin category, side
  margin((@theme[%(#{category}_margin_#{side})] || (side == :bottom ? @theme.vertical_spacing : 0)), side)
end
typeset_formatted_text(fragments, line_metrics, opts = {}) click to toggle source

QUESTION combine with typeset_text?

# File lib/asciidoctor-pdf/converter.rb, line 3354
def typeset_formatted_text fragments, line_metrics, opts = {}
  move_down line_metrics.padding_top
  formatted_text fragments, { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge(opts)
  move_down line_metrics.padding_bottom
end
typeset_text(string, line_metrics, opts = {}) click to toggle source

TODO document me, esp the first line formatting functionality

# File lib/asciidoctor-pdf/converter.rb, line 3341
def typeset_text string, line_metrics, opts = {}
  move_down line_metrics.padding_top
  opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
  if (first_line_opts = opts.delete :first_line_options)
    # TODO good candidate for Prawn enhancement!
    text_with_formatted_first_line string, first_line_opts, opts
  else
    text string, opts
  end
  move_down line_metrics.padding_bottom
end
width_of_fragments(fragments) click to toggle source

Calculate the width that is needed to print all the fragments without wrapping any lines.

This method assumes endlines are represented as discrete entries in the fragments array.

# File lib/asciidoctor-pdf/converter.rb, line 3302
def width_of_fragments fragments
  line_widths = [0]
  fragments.each do |fragment|
    if fragment.text == LF
      line_widths << 0
    else
      line_widths[-1] += fragment.width
    end
  end
  line_widths.max
end
write(pdf_doc, target) click to toggle source
# File lib/asciidoctor-pdf/converter.rb, line 3130
def write pdf_doc, target
  if target.respond_to? :write
    require_relative 'core_ext/quantifiable_stdout' unless defined? ::QuantifiableStdout
    target = ::QuantifiableStdout.new STDOUT if target == STDOUT
    pdf_doc.render target
  else
    pdf_doc.render_file target
    # QUESTION restore attributes first?
    @pdfmark.generate_file target if @pdfmark
  end
  # write scratch document if debug is enabled (or perhaps DEBUG_STEPS env)
  #get_scratch_document.render_file 'scratch.pdf'
  nil
end