class MultipageHtml5Converter

Attributes

full_outline[RW]

contains the entire outline of the top-level document, used as a guide-rail for creating TOC elements for documents we split off. Only expected to be set in the top-level converter (see AsciiDoctor::Document::mp_root)

pages[RW]

Public Class Methods

new(backend, opts = {}) click to toggle source
Calls superclass method
# File lib/asciidoctor-multipage.rb, line 112
def initialize(backend, opts = {})
  @xml_mode = false
  @void_element_slash = nil
  super
  @stylesheets = Stylesheets.instance
  @pages = []
end

Public Instance Methods

check_root(doc) click to toggle source

ensures that the AsciiDoctor::Document::mp_root is correctly set on the document object. The variable could have already been set if we created the document ourselves (see ::MultipageHtml5Converter::convert_section), in which case it's not changed. If the documented is “nested”, then we expect the parent document to already have it set. Otherwise, this is expected to be a top-level document, and we assign ourselves as its original converter.

# File lib/asciidoctor-multipage.rb, line 136
def check_root(doc)
  unless doc.mp_root
    if doc.nested?
      doc.mp_root = doc.parent_document.mp_root
    else
      doc.mp_root = self
    end
  end
end
convert_document(node) click to toggle source

Process Document (either the original full document or a processed page)

Calls superclass method
# File lib/asciidoctor-multipage.rb, line 147
def convert_document(node)

  # make sure document has original converter reference
  check_root(node)

  if node.processed
    # This node (an individual page) can now be handled by
    # Html5Converter.
    super
  else
    # This node is the original full document which has not yet been
    # processed; this is the entry point for the extension.

    # Turn off extensions to avoid running them twice.
    # FIXME: DocinfoProcessor, InlineMacroProcessor, and Postprocessor
    # extensions should be retained. Is this possible with the API?
    #Asciidoctor::Extensions.unregister_all

    # Check toclevels and multipage-level attributes
    mplevel = node.document.attr('multipage-level', 1).to_i
    toclevels = node.document.attr('toclevels', 2).to_i
    if toclevels < mplevel
      logger.warn 'toclevels attribute should be >= multipage-level'
    end
    if mplevel < 0
      logger.warn 'multipage-level attribute must be >= 0'
      mplevel = 0
    end
    node.document.set_attribute('multipage-level', mplevel.to_s)

    # Set multipage chunk types
    set_multipage_attrs(node)

    # FIXME: This can result in a duplicate ID without a warning.
    # Set the "id" attribute for the Document, using the "docname", which is
    # based on the file name. Then register the document ID using the
    # document title. This allows cross-references to refer to (1) the
    # top-level document itself or (2) anchors in top-level content (blocks
    # that are specified before any sections).
    node.id = node.attributes['docname']
    node.register(:refs, [node.id,
                          (Inline.new(parent = node,
                                      context = :anchor,
                                      text = node.doctitle,
                                      opts = {:type => :ref,
                                              :id => node.id})),
                          node.doctitle])

    # Generate navigation links for all pages
    generate_nav_links(node)

    # Create and save a skeleton document for generating the TOC lists,
    # but don't attempt to create outline for nested documents.
    unless node.nested?
      # if the original converter has the @full_outline set already, we are about
      # to replace it. That's not supposed to happen, and probably means we encountered
      # a document structure we aren't prepared for. Log an error and move on.
      logger.error "Regenerating document outline, something wrong?" if node.mp_root.full_outline
      node.mp_root.full_outline = new_outline_doc(node)
    end

    # Save the document catalog to use for each part/chapter page.
    @catalog = node.catalog

    # Retain any book intro blocks, delete others, and add a list of sections
    # for the book landing page.
    parts_list = Asciidoctor::List.new(node, :ulist)
    node.blocks.delete_if do |block|
      if block.context == :section
        part = block
        part.convert
        text = %(<<#{part.id},#{part.captioned_title}>>)
        if (desc = block.attr('desc')) then text << %( – #{desc}) end
        parts_list << Asciidoctor::ListItem.new(parts_list, text)
      end
    end
    node << parts_list

    # Add navigation links
    add_nav_links(node)

    # Mark page as processed and return converted result
    node.processed = true
    node.convert
  end
end
convert_embedded(node) click to toggle source

Process Document in embeddable mode (either the original full document or a processed page)

Calls superclass method
# File lib/asciidoctor-multipage.rb, line 236
def convert_embedded(node)
  # make sure document has original converter reference
  check_root(node)
  if node.processed
    # This node (an individual page) can now be handled by
    # Html5Converter.
    super
  else
    # This node is the original full document which has not yet been
    # processed; it can be handled by convert_document().
    convert_document(node)
  end
end
convert_inline_anchor(node) click to toggle source

Include chapter pages in cross-reference links. This method overrides for the :xref node type only.

Calls superclass method
# File lib/asciidoctor-multipage.rb, line 350
def convert_inline_anchor(node)
  if node.type == :xref
    # This is the same as super...
    if (path = node.attributes['path'])
      attrs = (append_link_constraint_attrs node, node.role ? [%( class="#{node.role}")] : []).join
      text = node.text || path
    else
      attrs = node.role ? %( class="#{node.role}") : ''
      unless (text = node.text)
        refid = node.attributes['refid']
        if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid])
          text = (ref.xreftext node.attr('xrefstyle')) || %([#{refid}])
        else
          text = %([#{refid}])
        end
      end
    end

    # But we also need to find the parent page of the target node.
    current = node.document.catalog[:refs][node.attributes['refid']]
    until current.respond_to?(:mplevel) && current.mplevel != :content
      return %(<a href="#{node.target}"#{attrs}>#{text}</a>) if !current
      current = current.parent
    end
    parent_page = current

    # If the target is the top-level section of the parent page, there is no
    # need to include the anchor.
    if "##{parent_page.id}" == node.target
      target = "#{parent_page.id}.html"
    else
      target = "#{parent_page.id}.html#{node.target}"
    end

    %(<a href="#{target}"#{attrs}>#{text}</a>)
  else
    # Other anchor types can be handled as normal.
    super
  end
end
convert_outline(node, opts = {}) click to toggle source

Override Html5Converter convert_outline() to return a custom TOC outline.

# File lib/asciidoctor-multipage.rb, line 432
def convert_outline(node, opts = {})
  doc = node.document
  # Find this node in the @full_outline skeleton document
  page_node = doc.mp_root.full_outline.find_by(id: node.id).first
  # Create a skeleton document for this particular page
  custom_outline_doc = new_outline_doc(doc.mp_root.full_outline, for_page: page_node)
  opts[:page_id] = node.id
  # Generate an extra TOC entry for the root page. Add additional styling if
  # the current page is the root page.
  root_file = %(#{doc.attr('docname')}#{doc.attr('outfilesuffix')})
  root_link = %(<a href="#{root_file}">#{doc.doctitle}</a>)
  classes = ['toc-root']
  classes << 'toc-current' if node.id == doc.attr('docname')
  root = %(<span class="#{classes.join(' ')}">#{root_link}</span>)
  # Create and return the HTML
  %(<p>#{root}</p>#{generate_outline(custom_outline_doc, opts)})
end
convert_section(node) click to toggle source

Process a Section. Each Section will either be split off into its own page or processed as normal by Html5Converter.

Calls superclass method
# File lib/asciidoctor-multipage.rb, line 476
def convert_section(node)
  doc = node.document
  if doc.processed
    # This node can now be handled by Html5Converter.
    super
  else
    # This node is from the original document and has not yet been processed.

    # Create a new page for this section
    page = Asciidoctor::Document.new([],
                                     {:attributes => doc.attributes.clone,
                                      :doctype => doc.doctype,
                                      :header_footer => !doc.attr?(:embedded),
                                      :safe => doc.safe})
    # Retain webfonts attribute (why is doc.attributes.clone not adequate?)
    page.set_attr('webfonts', doc.attr(:webfonts))
    # Save sectnum for use later (a Document object normally has no sectnum)
    if node.parent.respond_to?(:numbered) && node.parent.numbered
      page.sectnum = node.parent.sectnum
    end

    page.mp_root = doc.mp_root

    # Process node according to mplevel
    if node.mplevel == :branch
      # Retain any part intro blocks, delete others, and add a list
      # of sections for the part landing page.
      chapters_list = Asciidoctor::List.new(node, :ulist)
      node.blocks.delete_if do |block|
        if block.context == :section
          chapter = block
          chapter.convert
          text = %(<<#{chapter.id},#{chapter.captioned_title}>>)
          # NOTE, there is a non-breaking space (Unicode U+00A0) below.
          if desc = block.attr('desc') then text << %( – #{desc}) end
          chapters_list << Asciidoctor::ListItem.new(chapters_list, text)
          true
        end
      end
      # Add chapters list to node, reparent node to new page, add
      # node to page, mark as processed, and add page to @pages.
      node << chapters_list
      reparent(node, page)
      page.blocks << node
    else # :leaf
      # Reparent node to new page, add node to page, mark as
      # processed, and add page to @pages.
      reparent(node, page)
      page.blocks << node
    end

    # Add navigation links using saved HTML
    page.nav_links = node.nav_links
    add_nav_links(page)

    # Mark page as processed and add to collection of pages
    @pages << page
    page.id = node.id
    page.catalog = @catalog
    page.mplevel = node.mplevel
    page.processed = true
  end
end
generate_outline(node, opts = {}) click to toggle source

Generate the actual HTML outline for the TOC. This method is analogous to Html5Converter convert_outline().

# File lib/asciidoctor-multipage.rb, line 285
def generate_outline(node, opts = {})
  # Do the same as Html5Converter convert_outline() here
  return unless node.sections? && node.sections.length > 0
  sectnumlevels = opts[:sectnumlevels] || (node.document.attributes['sectnumlevels'] || 3).to_i
  toclevels = opts[:toclevels] || (node.document.attributes['toclevels'] || 2).to_i
  sections = node.sections
  result = [%(<ul class="sectlevel#{sections[0].level}">)]
  sections.each do |section|
    slevel = section.level
    if section.caption
      stitle = section.captioned_title
    elsif section.numbered && slevel <= sectnumlevels
      if slevel < 2 && node.document.doctype == 'book'
        if section.sectname == 'chapter'
          stitle =  %(#{(signifier = node.document.attributes['chapter-signifier']) ? "#{signifier} " : ''}#{section.sectnum} #{section.title})
        elsif section.sectname == 'part'
          stitle =  %(#{(signifier = node.document.attributes['part-signifier']) ? "#{signifier} " : ''}#{section.sectnum nil, ':'} #{section.title})
        else
          stitle = %(#{section.sectnum} #{section.title})
        end
      else
        stitle = %(#{section.sectnum} #{section.title})
      end
    else
      stitle = section.title
    end
    stitle = stitle.gsub DropAnchorRx, '' if stitle.include? '<a'

    # But add a special style for current page in TOC
    if section.id == opts[:page_id]
      stitle = %(<span class="toc-current">#{stitle}</span>)
    end

    # And we also need to find the parent page of the target node
    current = section
    until current.mplevel != :content
      current = current.parent
    end
    parent_chapter = current

    # If the target is the top-level section of the parent page, there is no
    # need to include the anchor.
    if parent_chapter.id == section.id
      link = %(#{parent_chapter.id}.html)
    else
      link = %(#{parent_chapter.id}.html##{section.id})
    end
    result << %(<li><a href="#{link}">#{stitle}</a>)

    # Finish in a manner similar to Html5Converter convert_outline()
    if slevel < toclevels &&
       (child_toc_level = generate_outline section,
                                           toclevels: toclevels,
                                           secnumlevels: sectnumlevels,
                                           page_id: opts[:page_id])
      result << child_toc_level
    end
    result << '</li>'
  end
  result << '</ul>'
  result.join LF
end
new_outline_doc(node, new_parent:nil, for_page:nil) click to toggle source

From node, create a skeleton document that will be used to generate the TOC. This is first used to create a full skeleton (@full_outline) from the original document (for_page=nil). Then it is used for each individual page to create a second skeleton from the first. In this way, TOC entries are included that are not part of the current page, or excluded if not applicable for the current page.

# File lib/asciidoctor-multipage.rb, line 397
def new_outline_doc(node, new_parent:nil, for_page:nil)
  if node.class == Document
    new_document = Document.new([])
    new_document.mplevel = node.mplevel
    new_document.id = node.id
    new_document.update_attributes(node.attributes)
    new_parent = new_document
    node.sections.each do |section|
      new_outline_doc(section, new_parent: new_parent,
                      for_page: for_page)
    end
  # Include the node if either (1) we are creating the full skeleton from the
  # original document or (2) the node is applicable to the current page.
  elsif !for_page ||
        node.related_to?(for_page)
    new_section = Section.new(parent = new_parent,
                              level = node.level,
                              numbered = node.numbered)
    new_section.id = node.id
    new_section.caption = node.caption
    new_section.title = node.instance_variable_get(:@title)
    new_section.mplevel = node.mplevel
    new_parent << new_section
    new_parent.sections.last.numeral = node.numeral
    new_parent = new_section
    node.sections.each do |section|
      new_outline_doc(section, new_parent: new_parent,
                      for_page: for_page)
    end
  end
  return new_document
end
reparent(node, parent) click to toggle source

Change node parent to new parent recursively

# File lib/asciidoctor-multipage.rb, line 451
def reparent(node, parent)
  node.parent = parent
  if node.context == :dlist
    node.find_by(context: :list_item).each do |block|
      reparent(block, node)
    end
  else
    node.blocks.each do |block|
      reparent(block, node)
      if block.context == :table
        block.columns.each do |col|
          col.parent = col.parent
        end
        block.rows.body.each do |row|
          row.each do |cell|
            cell.parent = cell.parent
          end
        end
      end
    end
  end
end
set_multipage_attrs(node) click to toggle source

Add multipage attribute to all sections in node, recursively.

# File lib/asciidoctor-multipage.rb, line 541
def set_multipage_attrs(node)
  doc = node.document
  node.mplevel = :root if node.class == Asciidoctor::Document
  node.sections.each do |section|
    # Check custom multipage-level attribute on section; warn and
    # discard if invalid
    if section.attr?('multipage-level', nil, false) &&
       section.attr('multipage-level').to_i <
       node.attr('multipage-level').to_i
      logger.warn %(multipage-level value specified for "#{section.id}" ) +
                  %(section cannot be less than the parent section value)
      section.set_attr('multipage-level', nil)
    end
    # Propagate custom multipage-level value to child node
    if !section.attr?('multipage-level', nil, false) &&
       node.attr('multipage-level') != doc.attr('multipage-level')
      section.set_attr('multipage-level', node.attr('multipage-level'))
    end
    # Set section type
    if section.level < section.attr('multipage-level', nil, true).to_i
      section.mplevel = :branch
    elsif section.level == section.attr('multipage-level', nil, true).to_i
      section.mplevel = :leaf
    else
      section.mplevel = :content
    end
    # Set multipage attribute on child sections now.
    set_multipage_attrs(section)
  end
end
write(output, target) click to toggle source

Convert each page and write it to file. Use filenames based on IDs.

# File lib/asciidoctor-multipage.rb, line 573
def write(output, target)
  # Write primary (book) landing page
  ::File.open(target, 'w') do |f|
    f.write(output)
  end
  # Write remaining part/chapter pages
  outdir = ::File.dirname(target)
  ext = ::File.extname(target)
  @pages.each do |doc|
    chapter_target = doc.id + ext
    outfile = ::File.join(outdir, chapter_target)
    ::File.open(outfile, 'w') do |f|
      f.write(doc.convert)
    end
  end
end