require 'nanoc/toolbox/helpers/html_tag'
module Nanoc::Toolbox::Helpers
# NANOC Helper for the Navigation related stuff.
#
# This module contains functions for generating navigation menus for your
# pages, like navigation menu, breadcrumbs or a table of content for a given Item
#
# @author Anouar ADLANI
module Navigation
include Nanoc::Helpers::LinkTo
include Nanoc::Helpers::Breadcrumbs
include Nanoc::Toolbox::Helpers::HtmlTag
# Generate a navigation menu for a given item.
# The menu will be generated form the identifier of the desired root element.
# The root itself will not be rendered. It generate the menu by parsing all
# the descendent of the passed item.
#
# @param [String] identifier - the identifier string of the root element
# @param [Hash] options - The Optional parameters
# @option options (see #render_menu)
# @option options [String] :kind ('article') The kind of items to display in the menu
#
# @return [String] The output ready to be displayed by the caller
def navigation_for(identifier, options={})
# Get root item for which we need to draw the navigation
root = @items.find { |i| i.identifier == identifier }
# Do not render if there is no child
return nil unless root.children
# Find all sections, and render them
sections = find_item_tree(root, options)
render_menu(sections, options)
end
# Generate a Table of Content for a given item. The toc will be generated
# form the item content. The parsing is done with Nokogiri through XPath.
#
# @param [Nanoc::ItemRep] item_rep - the representation of desired item
# @param [Hash] options - The Optional parameters
# @option options (see #render_menu)
# @option options [String] :path ('div[@class="section"]') Generic XPath for the sections
#
# @return [String] The output ready to be displayed by the caller
#
# @see http://nokogiri.org/
def toc_for(item_rep, options={})
require 'nokogiri'
item_rep = item_rep.rep_named(:default) if item_rep.is_a? Nanoc::Item
options[:path] ||= 'div[@class="section"]'
# Retreive the parsed content and init nokogiri
compiled_content = item_rep.instance_eval { @content[:pre] }
doc = Nokogiri::HTML(compiled_content)
doc_root = doc.xpath('/html/body').first
return "" if doc_root.nil?
# Find all sections, and render them
sections = find_toc_sections(doc_root, options[:path])
render_menu(sections, options) || ""
end
# Generate a Breadcrumb for a given item. The breadcrumbs, is starting with
# the root item and ending with the item itself.
#
# Requires the Helper: Nanoc::Helpers::Breadcrumbs
#
# @param [String] identifier - the identifier string of element
# @param [Hash] options - The Optional parameters
# @option options (see #render_menu)
#
# @return [String] The output ready to be displayed by the caller
#
# @see Nanoc::Helpers::Breadcrumbs#breadcrumbs_for_identifier
def breadcrumb_for(identifier, options={})
options[:collection_tag] ||= 'ul'
options[:collection_class] ||= 'breadcrumb'
# Retreive the breadcrumbs trail and format them
sections = find_breadcrumbs_trail(identifier)
render_menu(sections, options)
end
# Render a Hash to a HTML List by default
#
# Hash structure should be construct like this:
#
# Link: is an hash with the following key
# - :title => The content of the link
# - :link => The link
# - :subsections => nil or an Array of Links
#
# [{:title => 'Title', :link => 'http://example.com', :subsections => [{}, {}, ...]},{...}]
#
# Results to an output like the following (by default):
#
#
# @param [Array] items - The array of links that need to be rendered
# @param [Hash] options - The Optional parameters
# @option options [Interger] :depth (3) maximum depth of the rendered menu
# @option options [String] :collection_tag ('ol') tag englobing collection of items
# @option options [String] :item_tag ('li') tag englobing item
# @option options [String] :title_tag ('h2') tag englobing the title
# @option options [String] :title ('') Title of the menu, if nil will not display title
# @option options [String] :separator ('') Menu item separator
#
# @return [String] The output ready to be displayed by the caller
def render_menu(items, options={})
options[:depth] ||= 3
options[:collection_tag] ||= 'ol'
options[:collection_class] ||= 'menu'
options[:item_tag] ||= 'li'
options[:title_tag] ||= 'h2'
options[:title] ||= nil
options[:separator] ||= ''
# Parse the title and remove it from the options
title = options[:title] ? content_tag(options[:title_tag], options[:title]) : ''
options.delete(:title_tag)
options.delete(:title)
# Decrease the depth level
options[:depth] -= 1
rendered_menu = items.map do |item|
# Render only if there is depth left
if options[:depth].to_i > 0 && item[:subsections]
output = render_menu(item[:subsections], options)
options[:depth] += 1 # Increase the depth level after the call of navigation_for
end
output ||= ""
content_tag(options[:item_tag], link_to_unless_current(item[:title], item[:link]) + options[:separator] + output)
end.join()
title + content_tag(options[:collection_tag], rendered_menu, :class => options[:collection_class]) unless rendered_menu.strip.empty?
end
private
# Recursive method that extract from an XPath pattern the document structure
# and return the "permalinks" to each sections in an Array of Hash that
# could be used by the rendering method. The structure is deducted by the
# H1-6 header within the html element defined by the XPATH
def find_toc_sections(section, section_xpath, title_level=1)
return {} unless section.xpath(section_xpath)
# For each section found call the find_toc_sections on it with an
# increased header level (ex: h1 => h2) and then generate the hash res
sections = section.xpath(section_xpath).map do |subsection|
header = subsection.css("h1, h2, h3, h4, h5, h6").first
sub_id = subsection['id']
sub_title = header ? header.inner_html : 'untitled'
subsections = {}
if subsection.xpath("#{section_xpath}") && title_level <= 6
subsections = find_toc_sections(subsection, "#{section_xpath}", title_level+1)
end
{ :title => sub_title, :link => '#' + sub_id, :subsections => subsections }
end
end
# Recursive method that extract from an XPath pattern the document structure
# and return the "permalinks" in a Array of Hash that could be used by the
# rendering method
def find_item_tree(root, options={})
return nil unless root.children
# filter the elements to contain only the kind requested
children = options[:kind] ? root.children.select { |item| item[:kind] == options[:kind] } : root.children
# For each child call the find_item_tree on it and then generate the hash
sections = children.map do |child|
subsections = find_item_tree(child)
{ :title => (child[:title] || child.identifier),
:link => relative_path_to(child),
:subsections => subsections }
end
end
def find_breadcrumbs_trail(root)
trail = ["/"]
root.split('/').each { |s| trail << trail.last + "#{s}/" unless s.empty? }
trail.map do |child_identifier|
child = @items[child_identifier]
{ :title => (child[:short_title] || child[:title] || child.identifier),
:link => relative_path_to(child),
:subsections => nil } if child
end.compact
end
end
end