From 8e89ec504f804f805452a9e006db962f44a260b0 Mon Sep 17 00:00:00 2001 From: Arran Cudbard-Bell Date: Fri, 3 Jun 2011 23:21:59 -0700 Subject: [PATCH] Initial commit of Nokogiri based TOC generator --- lib/extensions/toc.rb | 132 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 lib/extensions/toc.rb diff --git a/lib/extensions/toc.rb b/lib/extensions/toc.rb new file mode 100644 index 00000000..ab1889f3 --- /dev/null +++ b/lib/extensions/toc.rb @@ -0,0 +1,132 @@ +module Gollum::Extensions + class Toc_gen + attr_accessor :header_tags + + NODE_CONTENT = 0 + NODE_ORDER = 1 + + def initialize (doc, settings = {}) + @doc = doc + @header_tags = ['title','h1','h2','h3','h4','h5','h6'] + end + + # Generates a table of contents for @doc using headers + # + # Returns Nokogiri::XML::Node if there were headers to parse, or nil if none were found + def generate + return unless (headings = find_headings).count > 0 + + node_count = 0 + lvl = 0 + @lvls = [].fill(0..@header_tags.count) {[]} + + headings.each do |h| + node_count += 1 + lvl_new = heading_to_lvl h + + @lvls[lvl_new].push [build_toc_node(h), node_count] + + # Pass in the highest level prior to flattening for efficiency + flatten_to_lvl(lvl, lvl_new) if lvl_new < lvl + + lvl = lvl_new + end + + flatten_to_lvl(lvl, 0) + end + + # Escape title string for use in ID attribute + # + # title - Title string + # + # Returns string + def insert_anchors + find_headings.each do |h| + rep_h = Nokogiri::XML::Node.new('a', @doc) + rep_h['name'] = anchor_id(h.content) + rep_h.add_child(h.clone) + h.replace(rep_h) + end + end + + # Escape title string for use in ID attribute + # + # title - Title string + # + # Returns string + def anchor_id (title) + CGI::escape(title) + end + + # Convert heading into list element + # + # heading - Nokogiri::XML::Node representing a single heading + # + # Returns string + def build_toc_node(heading) + xml_node = Nokogiri::XML::Node.new('li', @doc) + xml_node.add_child("" + heading.content + "") + xml_node + end + + # Search the current @doc for headings + # + # Returns Nokogiri::XML::NodeSet + def find_headings + @doc.css(@header_tags.join ' ,') + end + + # Convert specified heading to integer level + # + # heading - Nokogiri::XML::Node representing a single heading + # + # Returns int + def heading_to_lvl(heading) + heading.name.gsub('h','').to_i + end + + # Convert Nokogiri::XML:Nodes higher in the level array than the specified low_idx + # into a node tree, and either append or prepend the tree to the level specified + # by low_idx. + # + # low_idx - The level in the array to flatten to + # high - The highest level to flatten from + # + # Returns Nokogiri::XML::Nodes if low_idx is 0, else nil + def flatten_to_lvl(high_idx, low_idx = 0) + lvl = high_idx + 1 + lvl_up = nil + + @lvls[low_idx..high_idx].reverse_each do |lvl_children| + lvl -= 1 + next if lvl_children.count() == 0 || !lvl_up && (lvl_up = lvl) + + flatten_lvl(lvl_up,lvl) + lvl_up = lvl + end + + # Level 0 flattens all levels, and returns an XML node containing the TOC data + flatten_lvl(lvl_up, 0).first[NODE_CONTENT] if lvl == 0 && lvl_up + end + + # Insert all Nokogiri::XML::Nodes at lvl_src into a new ul XML::Node as children. + # Then either append or prepend the ul node, depending on whether the first child + # at the src_level appeared before or after the node in the dst_level in @doc + # + # lvl_src - The level in the array to src li XML::Nodes from + # lvl_dst - The level in the array to insert resulting ul XML::Node + # + # Returns array of Nokogiri::XML::Nodes @ lvl_dst + def flatten_lvl(lvl_src, lvl_dst) + lvl_sib = Nokogiri::XML::Node.new('ul', @doc) + src_pos = @lvls[lvl_src].first[NODE_ORDER] + @lvls[lvl_src].each {|lvl_child| lvl_sib.add_child(lvl_child[NODE_CONTENT])} + @lvls[lvl_src] = [] + + lvl_dst > 0 && src_pos < @lvls[lvl_dst].last[NODE_ORDER] ? + @lvls[lvl_dst].insert(-2,[lvl_sib, src_pos]) : + @lvls[lvl_dst].push([lvl_sib, src_pos]) + @lvls[lvl_dst] + end + end +end \ No newline at end of file