1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
|
# frozen_string_literal: true
require 'table_of_contents/helper'
module Jekyll
module TableOfContents
# Parse html contents and generate table of contents
class Parser
include ::Jekyll::TableOfContents::Helper
def initialize(html, options = {})
@doc = Nokogiri::HTML::DocumentFragment.parse(html)
@configuration = Configuration.new(options)
@entries = parse_content
end
def toc
build_toc + inject_anchors_into_html
end
def build_toc
%(<#{list_tag} id="#{@configuration.list_id}" class="#{@configuration.list_class}">\n#{build_toc_list(@entries)}</#{list_tag}>)
end
def inject_anchors_into_html
@entries.each do |entry|
# NOTE: `entry[:id]` is automatically URL encoded by Nokogiri
entry[:header_content].add_previous_sibling(
%(<a class="anchor" href="##{entry[:id]}" aria-hidden="true"><span class="octicon octicon-link"></span></a>)
)
end
@doc.inner_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
headers = Hash.new(0)
(@doc.css(toc_headings) - @doc.css(toc_headings_in_no_toc_section))
.reject { |n| n.classes.include?(@configuration.no_toc_class) }
.inject([]) do |entries, node|
text = node.text
id = node.attribute('id') || generate_toc_id(text)
suffix_num = headers[id]
headers[id] += 1
entries << {
id: suffix_num.zero? ? id : "#{id}-#{suffix_num}",
text: CGI.escapeHTML(text),
node_name: node.name,
header_content: node.children.first,
h_num: node.name.delete('h').to_i
}
end
end
# Returns the list items for entries
def build_toc_list(entries)
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="#{@configuration.item_class} #{@configuration.item_prefix}#{entry[:node_name]}"><a href="##{entry[:id]}">#{entry[:text]}</a>)
# If the next entry should be indented in the list, generate a sublist
next_i = i + 1
if next_i < entries.count && entries[next_i][:h_num] > min_h_num
nest_entries = get_nest_entries(entries[next_i, entries.count], min_h_num)
toc_list << %(\n<#{list_tag}#{ul_attributes}>\n#{build_toc_list(nest_entries)}</#{list_tag}>\n)
i += nest_entries.count
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)
toc_list << build_toc_list(nest_entries)
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
@configuration.toc_levels.map { |level| "h#{level}" }.join(',')
end
def toc_headings_in_no_toc_section
if @configuration.no_toc_section_class.is_a?(Array)
@configuration.no_toc_section_class.map { |cls| toc_headings_within(cls) }.join(',')
else
toc_headings_within(@configuration.no_toc_section_class)
end
end
def toc_headings_within(class_name)
@configuration.toc_levels.map { |level| ".#{class_name} h#{level}" }.join(',')
end
def ul_attributes
@ul_attributes ||= @configuration.sublist_class.empty? ? '' : %( class="#{@configuration.sublist_class}")
end
def list_tag
@list_tag ||= @configuration.ordered_list ? 'ol' : 'ul'
end
end
end
end
|