class MultipageHtml5Converter
Attributes
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)
Public Class Methods
# 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
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
Process Document (either the original full document or a processed page)
# 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
Process Document in embeddable mode (either the original full document or a processed page)
# 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
Include chapter pages in cross-reference links. This method overrides for the :xref node type only.
# 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
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
Process a Section. Each Section will either be split off into its own page or processed as normal by Html5Converter.
# 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 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
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
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
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
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