Commit e48d7c92 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Merge branch 'issue328-guideTOC'

parents 39057e05 cccb0218
Loading
Loading
Loading
Loading
+45 −27
Original line number Diff line number Diff line
require 'jsduck/util/html'
require 'jsduck/guide_toc_entry'

module JsDuck

  # Adds Table of Contents section to guide HTML.
  class GuideToc
    def initialize(html, guide_name, max_level=2)
      @html = html
      @guide_name = guide_name

      @min_level = 2
      @max_level = max_level

      @toc = GuideTocEntry.new
      @new_html = []
    end

    # Inserts table of contents at the top of guide HTML by looking
    # for <h2> elements.
    def self.inject(html, guide_name)
      toc = []
      new_html = []

      html.each_line do |line|
        if line =~ /^\s*<(h[1-6])>(.*?)<\/h[1-6]>$/
          tag = $1
    # for headings at or below the specified maximum level.
    def inject!
      @html.each_line do |line|
        if line =~ /^\s*<h([1-6])>(.*?)<\/h[1-6]>$/
          level = $1.to_i
          text = Util::HTML.strip_tags($2)
          id = guide_name + "-section-" + title_to_id(text)
          if tag == "h2"
            toc << "<li><a href='#!/guide/#{id}'>#{text}</a></li>\n"
          id = title_to_id(text)

          if include_to_toc?(level)
            @toc.add(level, id, text)
          end
          new_html << "<#{tag} id='#{id}'>#{text}</#{tag}>\n"

          @new_html << "<h#{level} id='#{id}'>#{text}</h#{level}>\n"
        else
          new_html << line
          @new_html << line
        end
      end

      # Inject TOC below first heading if at least 2 items in TOC
      if toc.length >= 2
        new_html.insert(1, [
            "<div class='toc'>\n",
            "<p><strong>Contents</strong></p>\n",
            "<ol>\n",
            toc,
            "</ol>\n",
            "</div>\n",
        ])
      inject_toc!

      @new_html.flatten.join
    end

      new_html.flatten.join
    private

    def include_to_toc?(level)
      (@min_level..@max_level).include?(level)
    end

    def self.title_to_id(title)
      CGI::escape(title.downcase.gsub(/ /, "-"))
    def title_to_id(title)
      @guide_name + "-section-" + CGI::escape(title.downcase.gsub(/ /, "-"))
    end

    # Injects TOC below first heading if at least 2 items in TOC
    def inject_toc!
      return if @toc.count < 2

      @new_html.insert(1, [
        "<div class='toc'>\n",
          "<p><strong>Contents</strong></p>\n",
          @toc.to_html,
        "</div>\n",
      ])
    end

  end
+54 −0
Original line number Diff line number Diff line
require 'jsduck/util/html'

module JsDuck

  # Manages the single TOC entry (with possible subentries).
  class GuideTocEntry
    attr_accessor :label, :items

    def initialize(parent=nil)
      @parent = parent
      @label = ""
      @items = []
      @min_level = 2
    end

    # Adds entry at the corresponding heading level.
    def add(level, id, text)
      if level == @min_level
        @items << GuideTocEntry.new(self)
        @items.last.label = "#{prefix} <a href='#!/guide/#{id}'>#{text}</a>\n"
      else
        if @items.empty?
          @items << GuideTocEntry.new(self)
        end
        @items.last.add(level-1, id, text)
      end
    end

    # Generates the heading counter, like "1.5.4."
    def prefix
      (@parent ? @parent.prefix : "") + "#{@items.length}."
    end

    # Total number of headings in TOC
    def count
      @items.map {|item| 1 + item.count}.reduce(0, :+)
    end

    # Converts to nested HTML list.
    def to_html
      return if @items.empty?

      return [
        "<ul>",
          @items.map do |item|
            "<li>#{item.label} #{item.to_html}</li>"
          end,
        "</ul>",
      ].flatten.compact.join("\n")
    end

  end

end
+1 −1
Original line number Diff line number Diff line
@@ -73,7 +73,7 @@ module JsDuck
      @formatter.doc_context = {:filename => guide[:filename], :linenr => 0}
      @formatter.images = Img::Dir.new(guide["url"], "guides/#{guide["name"]}")
      html = @formatter.format(Util::IO.read(guide[:filename]))
      html = GuideToc.inject(html, guide['name'])
      html = GuideToc.new(html, guide['name'], @opts.guides_toc_level).inject!
      html = GuideAnchors.transform(html, guide['name'])

      # Report unused images (but ignore the icon files)
+21 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ module JsDuck
    attr_accessor :message
    attr_accessor :welcome
    attr_accessor :guides
    attr_accessor :guides_toc_level
    attr_accessor :videos
    attr_accessor :examples
    attr_accessor :categories_path
@@ -112,6 +113,7 @@ module JsDuck
      @message = ""
      @welcome = nil
      @guides = nil
      @guides_toc_level = 2
      @videos = nil
      @examples = nil
      @categories_path = nil
@@ -393,6 +395,25 @@ module JsDuck
          @guides = canonical(path)
        end

        opts.on('--guides-toc-level=LEVEL',
          "Max heading level for guides' tables of contents.",
          "",
          "Values between 1 and 6 define the maximum level of a heading",
          "to be included into guides' Table of Contents:",
          "",
          "1 - Hides the table of contents.",
          "2 - <H2> headings are included.",
          "3 - <H2>,<H3> headings are included.",
          "4 - <H2>,<H3>,<H4> headings are included.",
          "5 - <H2>,<H3>,<H4>,<H5> headings are included.",
          "6 - <H2>,<H3>,<H4>,<H5>,<H6> headings are included.") do |level|
          @guides_toc_level = level.to_i
          if !(1..6).include? @guides_toc_level
            Logger.fatal("Unsupported --guides-toc-level: '#{level}'")
            exit(1)
          end
        end

        opts.on('--videos=PATH',
          "JSON file describing the videos.",
          "",
+106 −56
Original line number Diff line number Diff line
@@ -4,9 +4,24 @@ require "jsduck/guide_toc"
describe JsDuck::GuideToc do

  def inject(html)
    JsDuck::GuideToc.inject(html, "myguide")
    JsDuck::GuideToc.new(html, "myguide", max_level).inject!
  end

  describe "With max_level=1" do
    let(:max_level) { 2 }

    it "adds no toc section even when many H1 headings" do
      inject(<<-EOHTML).should_not =~ /<div class='toc'>/
        <h1>Chapter A</h2>
        <h1>Chapter B</h2>
        <h1>Chapter C</h2>
      EOHTML
    end
  end

  describe "With max_level=2" do
    let(:max_level) { 2 }

    it "adds no toc section when no headings" do
      inject("blah").should_not =~ /<div class='toc'>/
    end
@@ -75,5 +90,40 @@ describe JsDuck::GuideToc do
        <h5>Another Chapter</h5>
      EOHTML
    end
  end

  describe "With max_level=3" do
    let(:max_level) { 3 }

    it "adds TOC when one H2 heading and one H3 heading" do
      inject(<<-EOHTML).should =~ /<div class='toc'>/
        <h2>Chapter A</h2>
        <h3>Chapter A.A</h3>
      EOHTML
    end

    it "adds TOC when two H3 headings" do
      inject(<<-EOHTML).should =~ /<div class='toc'>/
        <h3>Chapter A.A</h3>
        <h3>Chapter A.B</h3>
      EOHTML
    end

    it "links H3 heading from TOC" do
      inject(<<-EOHTML).should =~ /<a href='#!\/guide\/myguide-section-h3-chapter'>/
        <h2>My Chapter</h2>
        <h3>H3 Chapter</h3>
      EOHTML
    end

    it "doesn't include any other headings besides H2 and H3 to TOC" do
      inject(<<-EOHTML).should_not =~ /<a href='#!\/guide\/myguide-section-my-chapter'>/
        <h2>Foo</h2>
        <h3>Bar</h3>
        <h4>My Chapter</h4>
        <h5>Another Chapter</h5>
      EOHTML
    end
  end

end
Loading