class Webby::Filters::Outline

The Outline filter is used to insert outline numbering into HTML heading tags (h1, h2, h3, etc.) and to generate a table of contents based on the heading tags. The table of contents is inserted into the page at the location of the <toc /> tag. If there is no <toc /> tag, then a table of contents will not be created but outline numbering will still take place.

If a table of contents is desired without outline number being inserted into the heading tags, this can be specified in the attibutes of the <toc /> tag itself.

<toc numbering="off" />

This will generate a table of contents, but not insert outline numbering into the heading tags.

The Outline filter will only work on valid HTML or XHTML pages. Therefore it should be used after any markup langauge filters (textile, markdown, etc.).

The following attributes can be specified in the <toc /> tag itself to control how outline numbering is performed by the filter. The attributes can be used in combination with one another.

numbering

If set to “off”, this will prevent numbers from being inserted into the page. The default is “on”.

<toc numbering="off" />

numbering_start

This is the number to start with when inserting outline numbers into a page. The default is 1.

<toc numbering_start="3" />

toc_style

The style of the Table of Contents list to generated. This will be either “ol” for an ordered list or “ul” for an unordered list. The default is an ordered list.

<toc toc_style="ul" />

toc_range

This limits the numbering to only a subset of the HTML heading tags. The defaul is to number all the heading tags.

<toc toc_range="h1-h3" />

In this example, only the heading tags h1, h2, and h3 will be numbered and included in the table of contents listing.

Example

Generate a table of contents using an unordered list, starting with the number 2, and only numbering heading levels 2, 3, and 4.

<toc numbering_start="2" toc_style="ul" toc_range="h2-h4" />

Public Class Methods

new( html ) click to toggle source

Creates a new outline filter that will operate on the given html string.

# File lib/webby/filters/outline.rb, line 77
def initialize( str )
  @str = str

  @cur_level, @base_level, @cur_depth = nil
  @level = [0] * 6
  @h_rgxp = /^h(\d)$/o

  @numbering = true
  @numbering_start = 1

  @toc = []
  @toc_style = 'ol'
  @toc_range = 'h1-h6'
  @list_opening = nil
end

Public Instance Methods

filter → html click to toggle source

Process the original html document passed to the filter when it was created. The document will be scanned for heading tags (h1, h2, etc.) and outline numbering and id attributes will be inserted. A table of contents will also be created and inserted into the page if a <toc /> tag is found.

For example, if there is a heading tag

<h3>Get Fuzzy</h3>

somewhere in a page about comic strips, the tag might be altered as such

<h3 id="h2_2_1"><span class="heading-num">2.2.1</span>Get Fuzzy</h3>

The id attribute is used to generate a linke from the table of contents to this particular heading tag. The original text of the tag is used in the table of contents – “Get Fuzzy” in this example.

# File lib/webby/filters/outline.rb, line 114
def filter
  doc = Hpricot.XML(@str)

  # extract directives from the "toc" tag
  toc_elem = doc.search('toc').first

  unless toc_elem.nil?
    @numbering = toc_elem['numbering'] !~ /off/i
    @numbering_start = Integer(toc_elem['numbering_start']) if toc_elem.has_attribute? 'numbering_start'
    @toc_style = toc_elem['toc_style'] if toc_elem.has_attribute? 'toc_style'
    @toc_range = toc_elem['toc_range'] if toc_elem.has_attribute? 'toc_range'
  end

  unless %w[ul ol].include? @toc_style
    raise ArgumentError, "unknown ToC list type '#{@toc_style}'"
  end

  m = /h(\d)\s*-\s*h(\d)/i.match @toc_range
  @toc_range = Integer(m[1])..Integer(m[2])
  @list_opening = build_list_opening(toc_elem)

  headers = @toc_range.map {|x| "h#{x}"}
  doc.traverse_element(*headers) do |elem|
    text, id = heading_info(elem)
    add_to_toc(text, id) if @toc_range.include? current_level
  end

  toc_elem.swap(toc) unless toc_elem.nil?
  doc.to_html
end

Private Instance Methods

add_to_toc( text, id ) click to toggle source

Add the given text and id reference to the table of contents.

# File lib/webby/filters/outline.rb, line 238
def add_to_toc( text, id )
  a = "<a href=\"##{id}\">#{h(text)}</a>"
  @toc << [depth, a]
end
build_list_opening( elem ) click to toggle source
# File lib/webby/filters/outline.rb, line 148
def build_list_opening( elem )
  lo = "<#{@toc_style}"
  unless elem.nil?
    %w[class style id].each do |atr|
      next unless elem.has_attribute? atr
      lo << " %s=\"%s\"" % [atr, elem[atr]]
    end
  end
  if @toc_style == 'ol' and @numbering_start != 1
    lo << " start=\"#{@numbering_start}\""
  end
  lo << ">"
end
current_level() click to toggle source

Returns the current heading level number.

# File lib/webby/filters/outline.rb, line 217
def current_level
  @cur_level
end
current_level=( level ) click to toggle source

Set the current heading level. This will set the label and depth as well. An error will be raised if the level is less than the base heading level.

The base heading level will be set to the level if it has not already been set. Therefore, the first heading tag encountered defines the base heading level.

# File lib/webby/filters/outline.rb, line 193
def current_level=( level )
  if @base_level.nil?
    @base_level = @cur_level = level
    @level[@base_level-1] = @numbering_start-1
  end

  if level < @base_level
    raise ::Webby::Error, "heading tags are not in order, cannot outline"
  end

  if level == @cur_level
    @level[level-1] += 1
  elsif level > @cur_level
    @cur_level.upto(level-1) {|ii| @level[ii] += 1}
  else
    @cur_level.downto(level+1) {|ii| @level[ii-1] = 0}
    @level[level-1] += 1
  end

  @cur_level = level
end
depth() click to toggle source

Return the nesting depth of the current heading level with respect to the base heading level. This is a one-based number.

# File lib/webby/filters/outline.rb, line 232
def depth
  @cur_level - @base_level + 1
end
heading_info( elem ) click to toggle source

Returns information for the given heading element. The information is returned as a two element array: [text, id].

This method will also insert outline numbering and an id attribute. The outline numbering can be disabled, but the id attribute must be present for TOC generation.

# File lib/webby/filters/outline.rb, line 169
def heading_info( elem )
  m = @h_rgxp.match(elem.name)
  level = Integer(m[1])

  self.current_level = level
  text = elem.inner_text

  lbl = label
  if numbering?
    elem.children.first.before %Q{<span class="heading-num">#{lbl}</span>}
  end
  elem['id'] = "h#{lbl.tr('.','_')}" if elem['id'].nil?

  return [text, elem['id']]
end
label() click to toggle source

Return the label string for the current heading level.

# File lib/webby/filters/outline.rb, line 223
def label
  rv = @level.dup
  rv.delete(0)
  rv.join('.')
end
numbering?() click to toggle source

Returns true if outline numbering should be inserted into the heading tags. Returns false otherwise.

# File lib/webby/filters/outline.rb, line 294
def numbering?
  @numbering
end
toc() click to toggle source

Returns the table of contents as a collection of nested ordered lists. This is fully formatted HTML.

# File lib/webby/filters/outline.rb, line 246
def toc
  ary = []

  lopen = "<#@toc_style>"
  lclose = "</#@toc_style>"
  prev_depth = open = 0

  @toc.each do |a|
    cur = a.first

    # close out the previous list item if we're at the same level
    if cur == prev_depth
      ary << "</li>"

    # if we are increasing the level, then start a new list
    elsif cur > prev_depth
      ary << if ary.empty? then @list_opening else lopen end
      open += 1

    # we are decreasing the level; close out tags but ensure we don't
    # close out all the tags (leave one open)
    else
      (prev_depth - cur).times {
        ary << "</li>" << lclose
        open -= 1
        break if open <= 0
      }
      if open > 0
        ary << "</li>"
      else
        ary << lopen
        open += 1
      end
    end

    # add the current element
    ary << "<li>" << a.last
    prev_depth = cur
  end

  # close out the remaingling tags
  ary << "</li>" << lclose
  ary.join("\n")
end