bhollis/maruku

View on GitHub
lib/maruku/toc.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module MaRuKu
  # A section in the table of contents of a document.
  class Section
    # The depth of the section (0 for toplevel).
    #
    # Equivalent to `header_element.level`.
    #
    # @return [Fixnum]
    attr_accessor :section_level

    # The nested section number, e.g. `[1, 2, 5]` for Section 1.2.5.
    #
    # @return [Array<Fixnum>]
    attr_accessor :section_number

    # The `:header` node for this section.
    # The value of `meta[:section]` for the header will be this node.
    #
    # @return [MDElement]
    attr_accessor :header_element

    # The immediate child nodes of this section.
    #
    # @todo Why does this never contain Strings?
    #
    # @return [Array<MDElement>]
    attr_accessor :immediate_children

    # The subsections of this section.
    #
    # @return [Array<Section>]
    attr_accessor :section_children

    def initialize
      @immediate_children = []
      @section_children = []
    end

    def inspect(indent = 1)
      if @header_element
        s = "\_" * indent <<
          "(#{@section_level})>\t #{@section_number.join('.')} : " <<
          @header_element.children_to_s <<
          " (id: '#{@header_element.attributes[:id]}')\n"
      else
        s = "Master\n"
      end
      @section_children.each {|c| s << c.inspect(indent + 1) }

      s
    end

    # Assign \{#section\_number section numbers}
    # to this section and its children.
    # This also assigns the section number attribute
    # to the sections' headers.
    #
    # This should only be called on the root section.
    #
    # @overload def numerate
    def numerate(a = [])
      self.section_number = a
      self.section_children.each_with_index {|c, i| c.numerate(a + [i + 1])}
      if h = self.header_element
        h.attributes[:section_number] = self.section_number
      end
    end


    # Returns an HTML representation of the table of contents.
    #
    # This should only be called on the root section.
    def to_html
      MaRuKu::Out::HTML::HTMLElement.new('div', { 'class' => 'maruku_toc' }, _to_html)
    end

    # Returns a LaTeX representation of the table of contents.
    #
    # This should only be called on the root section.
    def to_latex
      _to_latex + "\n\n"
    end

    protected

    def _to_html
      ul = MaRuKu::Out::HTML::HTMLElement.new('ul')
      @section_children.each do |c|
        li = MaRuKu::Out::HTML::HTMLElement.new('li')
        if span = c.header_element.render_section_number
          li << span
        end

        a = c.header_element.wrap_as_element('a')
        a.attributes.delete('id')
        a['href'] = "##{c.header_element.attributes[:id]}"

        li << a
        li << c._to_html if c.section_children.size > 0
        ul << li
      end
      ul
    end

    def _to_latex
      s = ""
      @section_children.each do |c|
        s << "\\noindent"
        if number = c.header_element.section_number
          s << number
        end
        id = c.header_element.attributes[:id]
        text = c.header_element.children_to_latex
        s << "\\hyperlink{#{id}}{#{text}}"
        s << "\\dotfill \\pageref*{#{id}} \\linebreak\n"
        s << c._to_latex if c.section_children.size > 0
      end
      s
    end
  end

  class MDDocument
    # The table of contents for the document.
    #
    # @return [Section]
    attr_accessor :toc

    # A map of header IDs to a count of how many times they've occurred in the document.
    #
    # @return [Hash<String, Number>]
    attr_accessor :header_ids
    
    def create_toc
      self.header_ids = Hash.new(0)

      each_element(:header) {|h| h.attributes[:id] ||= h.generate_id }


      # The root section
      s = Section.new
      s.section_level = 0

      stack = [s]

      i = 0
      while i < @children.size
        if children[i].node_type == :header
          header = @children[i]
          level = header.level
          s2 = Section.new
          s2.section_level = level
          s2.header_element = header
          header.instance_variable_set :@section, s2
          while level <= stack.last.section_level
            stack.pop
          end
          stack.last.section_children.push s2
          stack.push s2
        else
          stack.last.immediate_children.push @children[i]
        end
        i += 1
      end

      # If there is only one big header, then assume
      # it is the master
      if s.section_children.size == 1
        s = s.section_children.first
      end

      # Assign section numbers
      s.numerate

      s
    end
  end

  class MDElement
    # Generate an id for headers. Assumes @children is set.
    def generate_id
      raise "generate_id only makes sense for headers" unless node_type == :header
      generated_id = children_to_s.tr(' ', '_').downcase.gsub(/\W/, '').strip
      num_occurs = (@doc.header_ids[generated_id] += 1)
      generated_id += "_#{num_occurs}" if num_occurs > 1
      generated_id
    end
  end
end