From b1c34b5fab4401d2538e34c5a5ccb3b2bf9f8500 Mon Sep 17 00:00:00 2001 From: Nicolas Karg <50399433+N7K4@users.noreply.github.com> Date: Thu, 30 Apr 2020 06:13:09 +0200 Subject: [PATCH 01/31] Add configurable list_id for root toc-element --- lib/table_of_contents/configuration.rb | 84 +++++---- lib/table_of_contents/parser.rb | 252 ++++++++++++------------- 2 files changed, 169 insertions(+), 167 deletions(-) diff --git a/lib/table_of_contents/configuration.rb b/lib/table_of_contents/configuration.rb index 61eeeeb..1c17b2e 100644 --- a/lib/table_of_contents/configuration.rb +++ b/lib/table_of_contents/configuration.rb @@ -1,41 +1,43 @@ -# frozen_string_literal: true - -module Jekyll - module TableOfContents - # jekyll-toc configuration class - class Configuration - attr_accessor :toc_levels, :no_toc_class, :no_toc_section_class, - :list_class, :sublist_class, :item_class, :item_prefix - - DEFAULT_CONFIG = { - 'min_level' => 1, - 'max_level' => 6, - 'no_toc_section_class' => 'no_toc_section', - 'list_class' => 'section-nav', - 'sublist_class' => '', - 'item_class' => 'toc-entry', - 'item_prefix' => 'toc-' - }.freeze - - def initialize(options) - options = generate_option_hash(options) - - @toc_levels = options['min_level']..options['max_level'] - @no_toc_class = 'no_toc' - @no_toc_section_class = options['no_toc_section_class'] - @list_class = options['list_class'] - @sublist_class = options['sublist_class'] - @item_class = options['item_class'] - @item_prefix = options['item_prefix'] - end - - private - - def generate_option_hash(options) - DEFAULT_CONFIG.merge(options) - rescue TypeError - DEFAULT_CONFIG - end - end - end -end +# frozen_string_literal: true + +module Jekyll + module TableOfContents + # jekyll-toc configuration class + class Configuration + attr_accessor :toc_levels, :no_toc_class, :no_toc_section_class, + :list_class, :list_id, :sublist_class, :item_class, :item_prefix + + DEFAULT_CONFIG = { + 'min_level' => 1, + 'max_level' => 6, + 'no_toc_section_class' => 'no_toc_section', + 'list_class' => 'section-nav', + 'list_id' => 'toc', + 'sublist_class' => '', + 'item_class' => 'toc-entry', + 'item_prefix' => 'toc-' + }.freeze + + def initialize(options) + options = generate_option_hash(options) + + @toc_levels = options['min_level']..options['max_level'] + @no_toc_class = 'no_toc' + @no_toc_section_class = options['no_toc_section_class'] + @list_class = options['list_class'] + @list_id = options['list_id'] + @sublist_class = options['sublist_class'] + @item_class = options['item_class'] + @item_prefix = options['item_prefix'] + end + + private + + def generate_option_hash(options) + DEFAULT_CONFIG.merge(options) + rescue TypeError + DEFAULT_CONFIG + end + end + end +end diff --git a/lib/table_of_contents/parser.rb b/lib/table_of_contents/parser.rb index 036251d..cc5e2fe 100644 --- a/lib/table_of_contents/parser.rb +++ b/lib/table_of_contents/parser.rb @@ -1,126 +1,126 @@ -# frozen_string_literal: true - -module Jekyll - module TableOfContents - # Parse html contents and generate table of contents - class Parser - PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze - - 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 - %() - end - - def inject_anchors_into_html - @entries.each do |entry| - entry[:header_content].add_previous_sibling( - %() - ) - 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') || text - .downcase - .gsub(PUNCTUATION_REGEXP, '') # remove punctuation - .tr(' ', '-') # replace spaces with dash - - 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 << %(
  • #{entry[:text]}) - # 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\n#{build_toc_list(nest_entries)}\n) - i += nest_entries.count - end - # Add the closing tag for the current entry in the list - toc_list << %(
  • \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 - end - end -end +# frozen_string_literal: true + +module Jekyll + module TableOfContents + # Parse html contents and generate table of contents + class Parser + PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze + + 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 + %() + end + + def inject_anchors_into_html + @entries.each do |entry| + entry[:header_content].add_previous_sibling( + %() + ) + 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') || text + .downcase + .gsub(PUNCTUATION_REGEXP, '') # remove punctuation + .tr(' ', '-') # replace spaces with dash + + 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 << %(
  • #{entry[:text]}) + # 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\n#{build_toc_list(nest_entries)}\n) + i += nest_entries.count + end + # Add the closing tag for the current entry in the list + toc_list << %(
  • \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 + end + end +end From 9036248a2671fa1b391a00ffec83403ca5e712dd Mon Sep 17 00:00:00 2001 From: Nicolas Karg <50399433+N7K4@users.noreply.github.com> Date: Thu, 30 Apr 2020 06:21:57 +0200 Subject: [PATCH 02/31] Fix unit tests (add id="toc") --- test/parser/test_inject_anchors_filter.rb | 46 +- test/parser/test_option_error.rb | 80 +- test/parser/test_toc_filter.rb | 72 +- test/parser/test_toc_only_filter.rb | 46 +- test/parser/test_various_toc_html.rb | 886 +++++++++++----------- test/test_configuration.rb | 56 +- test/test_jekyll-toc.rb | 104 +-- test/test_toc_tag.rb | 56 +- 8 files changed, 674 insertions(+), 672 deletions(-) diff --git a/test/parser/test_inject_anchors_filter.rb b/test/parser/test_inject_anchors_filter.rb index c9158e5..d2f76b0 100644 --- a/test/parser/test_inject_anchors_filter.rb +++ b/test/parser/test_inject_anchors_filter.rb @@ -1,23 +1,23 @@ -# frozen_string_literal: true - -require 'test_helper' - -class TestInjectAnchorsFilter < Minitest::Test - include TestHelpers - - def setup - read_html_and_create_parser - end - - def test_injects_anchors_into_content - html = @parser.inject_anchors_into_html - - assert_match(%r{Simple H1}, html) - end - - def test_does_not_inject_toc - html = @parser.inject_anchors_into_html - - assert_nil(/