Skip to content

Instantly share code, notes, and snippets.

@SpaceOyster
Last active March 2, 2018 13:56
Show Gist options
  • Select an option

  • Save SpaceOyster/88ed46d2a93d93a7388a408690f69af6 to your computer and use it in GitHub Desktop.

Select an option

Save SpaceOyster/88ed46d2a93d93a7388a408690f69af6 to your computer and use it in GitHub Desktop.
Jekyll-TOC with Font Awesome. Based on [toshimaru/jekyll-toc](https://github.com/toshimaru/jekyll-toc)

Jekyll Table Of Contents PLugin

Based on toshimaru/jekyll-toc Requires nokogiri

Installation

  1. Add this line to *.gemspec file of your jekyll theme:
  spec.add_dependency "nokogiri"
  1. Copy jekyll-toc.rb file to _plugins directory of your theme

  2. Run in your terminal:

bundle install

Use it the same way as original plugin

Font awesome

Original plugin uses octicons, which wasn't sutable for my needs, so I used Font Awesome (Version 5.0.6) instead.

Added option

The only addition is this option, which toggles on/off TOC globaly:

toc:
  enabled: true

Decide yourself which way you want to configure TOC:

  1. you can turn off TOC globaly, and enable it manualy in a Front Matter of the post you want to generate TOC;
  2. or you can turn it on globaly, and desable it in posts, were it is requred.
spec.add_dependency "nokogiri" # Because nokogiri is required
require 'nokogiri'
module Jekyll
module TableOfContents
# Parse html contents and generate table of contents
class Parser
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u
DEFAULT_CONFIG = {
"min_level" => 1,
"max_level" => 6,
"enabled" => false,
}
def initialize(html, options = {})
@doc = Nokogiri::HTML::DocumentFragment.parse(html)
@toc_enabled_globaly = options["enabled"]
options = generate_option_hash(options)
@toc_levels = options["min_level"]..options["max_level"]
@entries = parse_content
end
def build_toc
%(<ul class="section-nav"><h3 class="toc-heading"><i class="far fa-list-alt" aria-hidden="true"></i>Contents</h3>\n#{build_toc_list(@entries, last_ul_used: true)}</ul>)
end
def inject_anchors_into_html
@entries.each do |entry|
entry[:content_node].add_next_sibling(%(<a id="#{entry[:id]}#{entry[:uniq]}" class="anchor" href="##{entry[:id]}#{entry[:uniq]}" aria-hidden="true"><i class="fa fa-link"></i></a>))
end
@doc.inner_html
end
def toc
build_toc + inject_anchors_into_html
end
private
# parse logic is from html-pipeline toc_filter
# https://github.com/jch/html-pipeline/blob/v1.1.0/lib/html/pipeline/toc_filter.rb
def parse_content
entries = []
headers = Hash.new(0)
# TODO: Use kramdown auto ids
@doc.css(toc_headings).each do |node|
text = node.text
id = text.downcase
id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
id.gsub!(' ', '-') # replace spaces with dash
uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1
header_content = node.children.last
next unless header_content
entries << {
id: id,
uniq: uniq,
text: text,
node_name: node.name,
content_node: header_content,
h_num: node.name.delete('h').to_i
}
end
entries
end
# Returns the list items for entries
def build_toc_list(entries, last_ul_used: false)
i = 0
toc_list = ''
min_h_num = entries.map { |e| e[:h_num] }.min
while i < entries.count
entry = entries[i]
if entry[:h_num] == min_h_num
# If the current entry should not be indented in the list, add the entry to the list
toc_list << %(<li class="toc-entry toc-#{entry[:node_name]}"><a href="##{entry[:id]}#{entry[:uniq]}">#{entry[:text]}</a>)
# If the next entry should be indented in the list, generate a sublist
if i + 1 < entries.count
next_entry = entries[i + 1]
if next_entry[:h_num] > min_h_num
nest_entries = get_nest_entries(entries[i + 1, entries.count], min_h_num)
toc_list << %(\n<ul>\n#{build_toc_list(nest_entries, last_ul_used: true)}</ul>\n)
i += nest_entries.count
end
end
# Add the closing tag for the current entry in the list
toc_list << %(</li>\n)
elsif entry[:h_num] > min_h_num
# If the current entry should be indented in the list, generate a sublist
nest_entries = get_nest_entries(entries[i, entries.count], min_h_num)
if last_ul_used
toc_list << build_toc_list(nest_entries, last_ul_used: true)
else
toc_list << %(<ul>\n#{build_toc_list(nest_entries, last_ul_used: true)}</ul>\n)
end
i += nest_entries.count - 1
end
i += 1
end
toc_list
end
# Returns the entries in a nested list
# The nested list starts at the first entry in entries (inclusive)
# The nested list ends at the first entry in entries with depth min_h_num or greater (exclusive)
def get_nest_entries(entries, min_h_num)
entries.inject([]) do |nest_entries, entry|
break nest_entries if entry[:h_num] == min_h_num
nest_entries << entry
end
end
def toc_headings
@toc_levels.map { |level| "h#{level}" }.join(",")
end
def generate_option_hash(options)
DEFAULT_CONFIG.merge(options)
rescue TypeError
DEFAULT_CONFIG
end
end
end
end
module Jekyll
class TocTag < Liquid::Tag
def render(context)
return unless context.registers[:page]['toc'] == true
content_html = context.registers[:page].content
::Jekyll::TableOfContents::Parser.new(content_html).build_toc
end
end
module TableOfContentsFilter
def toc_only(html)
return html unless toc_enabled?
::Jekyll::TableOfContents::Parser.new(html, toc_config).build_toc
end
def inject_anchors(html)
return html unless toc_enabled?
::Jekyll::TableOfContents::Parser.new(html, toc_config).inject_anchors_into_html
end
def toc(html)
return html unless toc_enabled?
::Jekyll::TableOfContents::Parser.new(html, toc_config).toc
end
private
def toc_enabled?
@context.registers[:page]['toc'] == true or @context.registers[:site].config['toc']['enabled'] == true
# @context.registers[:page]['toc'] == true
end
def toc_config
@context.registers[:site].config["toc"] || {}
# @context.registers[:site]['toc'] = true || false
end
end
end
Liquid::Template.register_filter(Jekyll::TableOfContentsFilter)
# Liquid::Template.register_tag('toc', Jekyll::TocTag) # will be enabled at v1.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment