├── .gitignore
├── CHANGELOG.md
├── lib
├── table_of_contents
│ ├── version.rb
│ ├── helper.rb
│ ├── configuration.rb
│ └── parser.rb
└── jekyll-toc.rb
├── Appraisals
├── Rakefile
├── Gemfile
├── gemfiles
├── jekyll_3.9.gemfile
├── jekyll_4.0.gemfile
├── jekyll_4.1.gemfile
├── jekyll_4.2.gemfile
└── jekyll_4.3.gemfile
├── .github
├── workflows
│ ├── rubocop.yml
│ ├── coverage.yml
│ └── ci.yml
└── dependabot.yml
├── test
├── parser
│ ├── test_toc_only_filter.rb
│ ├── test_inject_anchors_filter.rb
│ ├── test_invalid_options.rb
│ ├── test_toc_filter.rb
│ ├── test_ordered_list.rb
│ └── test_various_toc_html.rb
├── test_helper.rb
├── test_toc_tag.rb
├── test_configuration.rb
├── test_jekyll-toc.rb
└── test_kramdown_list.rb
├── .rubocop.yml
├── LICENSE.md
├── jekyll-toc.gemspec
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | /Gemfile.lock
3 | /.bundle/
4 | /coverage
5 | *.gemfile.lock
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog is maintained under [Github Releases](https://github.com/toshimaru/jekyll-toc/releases).
2 |
--------------------------------------------------------------------------------
/lib/table_of_contents/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Jekyll
4 | module TableOfContents
5 | VERSION = '0.19.0'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | SUPPORTED_VERSIONS = %w[3.9 4.0 4.1 4.2 4.3].freeze
4 |
5 | SUPPORTED_VERSIONS.each do |version|
6 | appraise "jekyll-#{version}" do
7 | gem 'jekyll', version
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 | task default: :test
4 |
5 | require 'rake/testtask'
6 | Rake::TestTask.new(:test) do |test|
7 | test.libs << 'lib' << 'test'
8 | test.pattern = 'test/**/test_*.rb'
9 | test.verbose = true
10 | end
11 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem 'appraisal'
6 | gem 'minitest-reporters'
7 | gem 'minitest'
8 | gem 'pry'
9 | gem 'rake'
10 | gem 'rubocop-minitest'
11 | gem 'rubocop-performance'
12 | gem 'rubocop-rake'
13 | gem 'rubocop'
14 | gem 'simplecov', '~> 0.22.0'
15 |
--------------------------------------------------------------------------------
/gemfiles/jekyll_3.9.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest-reporters"
7 | gem "minitest"
8 | gem "pry"
9 | gem "rake"
10 | gem "rubocop-minitest"
11 | gem "rubocop-performance"
12 | gem "rubocop-rake"
13 | gem "rubocop"
14 | gem "simplecov", "~> 0.22.0"
15 | gem "jekyll", "3.9"
16 |
17 | gemspec path: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/jekyll_4.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest-reporters"
7 | gem "minitest"
8 | gem "pry"
9 | gem "rake"
10 | gem "rubocop-minitest"
11 | gem "rubocop-performance"
12 | gem "rubocop-rake"
13 | gem "rubocop"
14 | gem "simplecov", "~> 0.22.0"
15 | gem "jekyll", "4.0"
16 |
17 | gemspec path: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/jekyll_4.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest-reporters"
7 | gem "minitest"
8 | gem "pry"
9 | gem "rake"
10 | gem "rubocop-minitest"
11 | gem "rubocop-performance"
12 | gem "rubocop-rake"
13 | gem "rubocop"
14 | gem "simplecov", "~> 0.22.0"
15 | gem "jekyll", "4.1"
16 |
17 | gemspec path: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/jekyll_4.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest-reporters"
7 | gem "minitest"
8 | gem "pry"
9 | gem "rake"
10 | gem "rubocop-minitest"
11 | gem "rubocop-performance"
12 | gem "rubocop-rake"
13 | gem "rubocop"
14 | gem "simplecov", "~> 0.22.0"
15 | gem "jekyll", "4.2"
16 |
17 | gemspec path: "../"
18 |
--------------------------------------------------------------------------------
/gemfiles/jekyll_4.3.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest-reporters"
7 | gem "minitest"
8 | gem "pry"
9 | gem "rake"
10 | gem "rubocop-minitest"
11 | gem "rubocop-performance"
12 | gem "rubocop-rake"
13 | gem "rubocop"
14 | gem "simplecov", "~> 0.22.0"
15 | gem "jekyll", "4.3"
16 |
17 | gemspec path: "../"
18 |
--------------------------------------------------------------------------------
/.github/workflows/rubocop.yml:
--------------------------------------------------------------------------------
1 | name: RuboCop
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | rubocop:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up Ruby
15 | uses: ruby/setup-ruby@v1
16 | with:
17 | ruby-version: 3.2
18 | bundler-cache: true
19 | - name: Run RuboCop
20 | run: bundle exec rubocop
21 |
--------------------------------------------------------------------------------
/lib/table_of_contents/helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Jekyll
4 | module TableOfContents
5 | # helper methods for Parser
6 | module Helper
7 | PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze
8 |
9 | def generate_toc_id(text)
10 | text = text.downcase
11 | .gsub(PUNCTUATION_REGEXP, '') # remove punctuation
12 | .tr(' ', '-') # replace spaces with dash
13 | CGI.escape(text)
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/parser/test_toc_only_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestTOCOnlyFilter < Minitest::Test
6 | include TestHelpers
7 |
8 | def setup
9 | read_html_and_create_parser
10 | end
11 |
12 | def test_injects_toc_container
13 | html = @parser.build_toc
14 |
15 | assert_includes(html, %(
))
16 | end
17 |
18 | def test_does_not_return_content
19 | html = @parser.build_toc
20 |
21 | refute_includes(html, %(Simple H1
))
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 | SimpleCov.start
5 |
6 | require 'minitest/autorun'
7 | require 'minitest/reporters'
8 | Minitest::Reporters.use!
9 |
10 | require 'jekyll'
11 | require 'jekyll-toc'
12 |
13 | SIMPLE_HTML = <<~HTML
14 | Simple H1
15 | Simple H2
16 | Simple H3
17 | Simple H4
18 | Simple H5
19 | Simple H6
20 | HTML
21 |
22 | module TestHelpers
23 | def read_html_and_create_parser(options = {})
24 | @parser = Jekyll::TableOfContents::Parser.new(SIMPLE_HTML, options)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: Coverage
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | coverage:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up Ruby
15 | uses: ruby/setup-ruby@v1
16 | with:
17 | ruby-version: 3.2
18 | bundler-cache: true
19 | - uses: paambaati/codeclimate-action@v6.0.0
20 | env:
21 | CC_TEST_REPORTER_ID: 6b81e393ea6ad38560386f650ea2fb0e57a7beb5e20f8c8364fabee30d5bff07
22 | with:
23 | coverageCommand: bundle exec rake
24 |
--------------------------------------------------------------------------------
/test/parser/test_inject_anchors_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestInjectAnchorsFilter < Minitest::Test
6 | include TestHelpers
7 |
8 | def setup
9 | read_html_and_create_parser
10 | end
11 |
12 | def test_injects_anchors_into_content
13 | html = @parser.inject_anchors_into_html
14 |
15 | assert_match(%r{Simple H1}, html)
16 | end
17 |
18 | def test_does_not_inject_toc
19 | html = @parser.inject_anchors_into_html
20 |
21 | refute_includes(html, %())
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "bundler"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | reviewers:
13 | - toshimaru
14 | - package-ecosystem: "github-actions"
15 | directory: "/"
16 | schedule:
17 | interval: "weekly"
18 | reviewers:
19 | - toshimaru
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | strategy:
12 | matrix:
13 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
14 | gemfile:
15 | - gemfiles/jekyll_3.9.gemfile
16 | - gemfiles/jekyll_4.0.gemfile
17 | - gemfiles/jekyll_4.1.gemfile
18 | - gemfiles/jekyll_4.2.gemfile
19 | - gemfiles/jekyll_4.3.gemfile
20 | env:
21 | BUNDLE_GEMFILE: ${{ matrix.gemfile }}
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | - name: Set up Ruby ${{ matrix.ruby }}
26 | uses: ruby/setup-ruby@v1
27 | with:
28 | ruby-version: ${{ matrix.ruby }}
29 | bundler-cache: true
30 | - name: Run Test
31 | run: bundle exec rake
32 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.7
3 | NewCops: enable
4 | Exclude:
5 | - "*.gemspec"
6 | - "gemfiles/*"
7 | - "vendor/**/*"
8 | - Rakefile
9 | - Gemfile
10 | require:
11 | - rubocop-minitest
12 | - rubocop-rake
13 | - rubocop-performance
14 |
15 | Metrics/MethodLength:
16 | Enabled: false
17 | Metrics/AbcSize:
18 | Enabled: false
19 | Metrics/ClassLength:
20 | Enabled: false
21 |
22 | Naming/FileName:
23 | Enabled: false
24 |
25 | Layout/LineLength:
26 | Enabled: false
27 | Layout/SpaceAroundMethodCallOperator:
28 | Enabled: true
29 |
30 | Lint/RaiseException:
31 | Enabled: true
32 | Lint/StructNewOverride:
33 | Enabled: true
34 |
35 | Style/WordArray:
36 | Enabled: false
37 | Style/HashEachMethods:
38 | Enabled: true
39 | Style/HashTransformKeys:
40 | Enabled: true
41 | Style/HashTransformValues:
42 | Enabled: true
43 | Style/ExponentialNotation:
44 | Enabled: true
45 |
--------------------------------------------------------------------------------
/test/parser/test_invalid_options.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestInvalidOptions < Minitest::Test
6 | BASE_HTML = 'h1
'
7 | EXPECTED_HTML = <<~HTML.chomp
8 |
11 | HTML
12 |
13 | def test_option_is_nil
14 | parser = Jekyll::TableOfContents::Parser.new(BASE_HTML, nil)
15 |
16 | assert_equal(EXPECTED_HTML, parser.build_toc)
17 | end
18 |
19 | def test_option_is_epmty_string
20 | parser = Jekyll::TableOfContents::Parser.new(BASE_HTML, '')
21 |
22 | assert_equal(EXPECTED_HTML, parser.build_toc)
23 | end
24 |
25 | def test_option_is_string
26 | parser = Jekyll::TableOfContents::Parser.new(BASE_HTML, 'string')
27 |
28 | assert_equal(EXPECTED_HTML, parser.build_toc)
29 | end
30 |
31 | def test_option_is_array
32 | parser = Jekyll::TableOfContents::Parser.new(BASE_HTML, [])
33 |
34 | assert_equal(EXPECTED_HTML, parser.build_toc)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/parser/test_toc_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestTOCFilter < Minitest::Test
6 | include TestHelpers
7 |
8 | def setup
9 | read_html_and_create_parser
10 | end
11 |
12 | def test_injects_anchors
13 | html = @parser.toc
14 |
15 | assert_match(%r{Simple H1}, html)
16 | end
17 |
18 | def test_nested_toc
19 | doc = Nokogiri::HTML(@parser.toc)
20 | nested_h6_text = doc.css('ul.section-nav')
21 | .css('li.toc-h1')
22 | .css('li.toc-h2')
23 | .css('li.toc-h3')
24 | .css('li.toc-h4')
25 | .css('li.toc-h5')
26 | .css('li.toc-h6')
27 | .text
28 |
29 | assert_equal('Simple H6', nested_h6_text)
30 | end
31 |
32 | def test_injects_toc_container
33 | html = @parser.toc
34 |
35 | assert_match(//, html)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020-2021 Toshimaru
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/test_toc_tag.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestTableOfContentsTag < Minitest::Test
6 | include Liquid
7 |
8 | def setup
9 | @stubbed_context = Struct.new(:registers)
10 | @stubbed_context1 = Struct.new(:config)
11 | @stubbed_context2 = Struct.new(:toc, :content)
12 | end
13 |
14 | def test_toc_tag
15 | context = @stubbed_context.new({
16 | page: @stubbed_context2.new({ 'toc' => false }, 'test
'),
17 | site: @stubbed_context1.new({ 'toc' => nil })
18 | })
19 | tag = Jekyll::TocTag.parse('toc_tag', '', Tokenizer.new(''), ParseContext.new)
20 |
21 | assert_equal(%(), tag.render(context))
22 | end
23 |
24 | def test_toc_tag_returns_empty_string
25 | context = @stubbed_context.new({ page: { 'toc' => false } })
26 | tag = Jekyll::TocTag.parse('toc_tag', '', Tokenizer.new(''), ParseContext.new)
27 |
28 | assert_empty tag.render(context)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/test_configuration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestConfiguration < Minitest::Test
6 | def test_default_configuration
7 | configuration = Jekyll::TableOfContents::Configuration.new({})
8 |
9 | assert_equal(1..6, configuration.toc_levels)
10 | refute(configuration.ordered_list)
11 | assert_equal('no_toc_section', configuration.no_toc_section_class)
12 | assert_equal('toc', configuration.list_id)
13 | assert_equal('section-nav', configuration.list_class)
14 | assert_equal('', configuration.sublist_class)
15 | assert_equal('toc-entry', configuration.item_class)
16 | assert_equal('toc-', configuration.item_prefix)
17 | end
18 |
19 | def test_type_error
20 | configuration = Jekyll::TableOfContents::Configuration.new('TypeError!')
21 |
22 | assert_equal(1..6, configuration.toc_levels)
23 | refute(configuration.ordered_list)
24 | assert_equal('no_toc_section', configuration.no_toc_section_class)
25 | assert_equal('toc', configuration.list_id)
26 | assert_equal('section-nav', configuration.list_class)
27 | assert_equal('', configuration.sublist_class)
28 | assert_equal('toc-entry', configuration.item_class)
29 | assert_equal('toc-', configuration.item_prefix)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/jekyll-toc.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/table_of_contents/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'jekyll-toc'
7 | spec.version = Jekyll::TableOfContents::VERSION
8 | spec.summary = 'Jekyll Table of Contents plugin'
9 | spec.description = 'Jekyll (Ruby static website generator) plugin which generates a Table of Contents for the page.'
10 | spec.authors = %w[toshimaru torbjoernk]
11 | spec.email = 'me@toshimaru.net'
12 | spec.homepage = 'https://github.com/toshimaru/jekyll-toc'
13 | spec.license = 'MIT'
14 | spec.require_paths = ['lib']
15 |
16 | spec.metadata['homepage_uri'] = spec.homepage
17 | spec.metadata['source_code_uri'] = 'https://github.com/toshimaru/jekyll-toc'
18 | spec.metadata['changelog_uri'] = 'https://github.com/toshimaru/jekyll-toc/releases'
19 | spec.metadata['rubygems_mfa_required'] = 'true'
20 |
21 | # Specify which files should be added to the gem when it is released.
22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23 | spec.files = Dir.chdir(__dir__) do
24 | `git ls-files -z`.split("\x0").reject do |f|
25 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
26 | end
27 | end
28 |
29 | spec.required_ruby_version = '>= 2.7'
30 |
31 | spec.add_dependency 'jekyll', '>= 3.9'
32 | spec.add_dependency 'nokogiri', '~> 1.12'
33 | end
34 |
--------------------------------------------------------------------------------
/lib/table_of_contents/configuration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Jekyll
4 | module TableOfContents
5 | # jekyll-toc configuration class
6 | class Configuration
7 | attr_reader :toc_levels, :no_toc_class, :ordered_list, :no_toc_section_class,
8 | :list_id, :list_class, :sublist_class, :item_class, :item_prefix
9 |
10 | DEFAULT_CONFIG = {
11 | 'min_level' => 1,
12 | 'max_level' => 6,
13 | 'ordered_list' => false,
14 | 'no_toc_section_class' => 'no_toc_section',
15 | 'list_id' => 'toc',
16 | 'list_class' => 'section-nav',
17 | 'sublist_class' => '',
18 | 'item_class' => 'toc-entry',
19 | 'item_prefix' => 'toc-'
20 | }.freeze
21 |
22 | def initialize(options)
23 | options = generate_option_hash(options)
24 |
25 | @toc_levels = options['min_level']..options['max_level']
26 | @ordered_list = options['ordered_list']
27 | @no_toc_class = 'no_toc'
28 | @no_toc_section_class = options['no_toc_section_class']
29 | @list_id = options['list_id']
30 | @list_class = options['list_class']
31 | @sublist_class = options['sublist_class']
32 | @item_class = options['item_class']
33 | @item_prefix = options['item_prefix']
34 | end
35 |
36 | private
37 |
38 | def generate_option_hash(options)
39 | DEFAULT_CONFIG.merge(options)
40 | rescue TypeError
41 | DEFAULT_CONFIG
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/test_jekyll-toc.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestTableOfContentsFilter < Minitest::Test
6 | include Jekyll::TableOfContentsFilter
7 |
8 | DUMMY_HTML = 'Dummy HTML Content
'
9 |
10 | def test_toc_only
11 | @context = disable_toc_context
12 |
13 | assert_empty toc_only(DUMMY_HTML)
14 | end
15 |
16 | def test_inject_anchors
17 | @context = disable_toc_context
18 |
19 | assert_equal DUMMY_HTML, inject_anchors(DUMMY_HTML)
20 | end
21 |
22 | def test_toc
23 | @context = disable_toc_context
24 |
25 | assert_equal DUMMY_HTML, toc(DUMMY_HTML)
26 | end
27 |
28 | def test_toc_only2
29 | @context = enable_toc_context
30 |
31 | assert_equal %(), toc_only(DUMMY_HTML)
32 | end
33 |
34 | def test_inject_anchors2
35 | @context = enable_toc_context
36 |
37 | assert_equal DUMMY_HTML, inject_anchors(DUMMY_HTML)
38 | end
39 |
40 | def test_toc2
41 | @context = enable_toc_context
42 |
43 | assert_equal %(#{DUMMY_HTML}), toc(DUMMY_HTML)
44 | end
45 |
46 | private
47 |
48 | def disable_toc_context
49 | Struct.new(:registers).new({ page: { 'toc' => false } })
50 | end
51 |
52 | def enable_toc_context
53 | Struct.new(:registers).new({
54 | page: { 'toc' => true },
55 | site: Struct.new(:config).new({ 'toc' => false })
56 | })
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/jekyll-toc.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'nokogiri'
4 | require 'table_of_contents/configuration'
5 | require 'table_of_contents/parser'
6 |
7 | module Jekyll
8 | # toc tag for Jekyll
9 | class TocTag < Liquid::Tag
10 | def render(context)
11 | return '' unless context.registers[:page]['toc']
12 |
13 | content_html = context.registers[:page]['content']
14 | toc_config = context.registers[:site].config['toc'] || {}
15 | TableOfContents::Parser.new(content_html, toc_config).build_toc
16 | end
17 | end
18 |
19 | # Jekyll Table of Contents filter plugin
20 | module TableOfContentsFilter
21 | # Deprecated method. Removed in v1.0.
22 | def toc_only(html)
23 | return '' unless toc_enabled?
24 |
25 | TableOfContents::Parser.new(html, toc_config).build_toc
26 | end
27 |
28 | def inject_anchors(html)
29 | return html unless toc_enabled?
30 |
31 | TableOfContents::Parser.new(html, toc_config).inject_anchors_into_html
32 | end
33 |
34 | def toc(html)
35 | return html unless toc_enabled?
36 |
37 | TableOfContents::Parser.new(html, toc_config).toc
38 | end
39 |
40 | private
41 |
42 | def toc_enabled?
43 | @context.registers[:page]['toc'] == true
44 | end
45 |
46 | def toc_config
47 | @context.registers[:site].config['toc'] || {}
48 | end
49 | end
50 | end
51 |
52 | Liquid::Template.register_filter(Jekyll::TableOfContentsFilter)
53 | Liquid::Template.register_tag('toc', Jekyll::TocTag) # will be enabled at v1.0
54 |
--------------------------------------------------------------------------------
/test/parser/test_ordered_list.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestOrderedList < Minitest::Test
6 | include TestHelpers
7 |
8 | def test_default_configuration
9 | configuration = Jekyll::TableOfContents::Configuration.new({})
10 |
11 | refute(configuration.ordered_list)
12 | end
13 |
14 | def test_disabled_ordered_list
15 | configuration = Jekyll::TableOfContents::Configuration.new('ordered_list' => false)
16 |
17 | refute(configuration.ordered_list)
18 | end
19 |
20 | def test_enabled_ordered_list
21 | configuration = Jekyll::TableOfContents::Configuration.new('ordered_list' => true)
22 |
23 | assert(configuration.ordered_list)
24 | end
25 |
26 | def test_basic_ordered_list_top_heading
27 | parse_with_ordered_list
28 | html = @parser.toc
29 |
30 | assert_match(/^/, html)
31 | end
32 |
33 | def test_ordered_list_sub_headings
34 | parse_with_ordered_list
35 | html = @parser.toc
36 |
37 | assert_match(/\n- /, html)
45 | end
46 |
47 | def test_ordered_list_sub_headings_with_classes
48 | parse_with_ordered_list_and_classes
49 | html = @parser.toc
50 |
51 | assert_match(/
/, html)
52 | end
53 |
54 | def test_ordered_list_subheadings_with_classes_nested_structure
55 | parse_with_ordered_list_and_classes
56 | html = @parser.toc
57 |
58 | occurrences = html.scan('').count
59 |
60 | assert_equal(5, occurrences)
61 | end
62 |
63 | private
64 |
65 | def parse_with_ordered_list
66 | read_html_and_create_parser('ordered_list' => true)
67 | end
68 |
69 | def parse_with_ordered_list_and_classes
70 | read_html_and_create_parser(
71 | 'ordered_list' => true,
72 | 'list_class' => 'top-list-class',
73 | 'sublist_class' => 'sublist-class'
74 | )
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/test_kramdown_list.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestKramdownList < Minitest::Test
6 | # NOTE: kramdown automatically injects `id` attribute
7 | def test_kramdown_heading
8 | text = <<~MARKDOWN
9 | # h1
10 |
11 | ## h2
12 | MARKDOWN
13 | expected = <<~HTML
14 | h1
15 |
16 | h2
17 | HTML
18 | actual = Kramdown::Document.new(text).to_html
19 |
20 | assert_equal(expected, actual)
21 | end
22 |
23 | def test_japanese_heading
24 | text = <<~MARKDOWN
25 | # 日本語見出し1
26 |
27 | ## 日本語見出し2
28 | MARKDOWN
29 | expected = <<~HTML
30 | 日本語見出し1
31 |
32 | 日本語見出し2
33 | HTML
34 | actual = Kramdown::Document.new(text).to_html
35 |
36 | assert_equal(expected, actual)
37 | end
38 |
39 | def test_kramdown_list_l1_l5
40 | text = <<~MARKDOWN
41 | * level-1
42 | * level-2
43 | * level-3
44 | * level-4
45 | * level-5
46 | MARKDOWN
47 | expected = <<~HTML
48 |
49 | - level-1
50 |
51 | - level-2
52 |
53 | - level-3
54 |
55 | - level-4
56 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | HTML
68 | actual = Kramdown::Document.new(text).to_html
69 |
70 | assert_equal(expected, actual)
71 | end
72 |
73 | def test_kramdown_list_l1_l3_l2_l4
74 | text = <<~MARKDOWN
75 | * level-1
76 | * level-3
77 | * level-2
78 | * level-4
79 | * level-5
80 | MARKDOWN
81 | expected = <<~HTML
82 |
83 | - level-1
84 |
85 | - level-3
86 | - level-2
87 |
88 | - level-4
89 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | HTML
99 | actual = Kramdown::Document.new(text).to_html
100 |
101 | assert_equal(expected, actual)
102 | end
103 |
104 | def test_kramdown_list_l4_l1
105 | text = <<~MARKDOWN
106 | * level-4
107 | * level-3
108 | * level-2
109 | * level-1
110 | MARKDOWN
111 | expected = <<~HTML
112 | * level-4
113 | * level-3 * level-2 * level-1
114 |
115 | HTML
116 | actual = Kramdown::Document.new(text).to_html
117 |
118 | assert_equal(expected, actual)
119 | end
120 |
121 | def test_kramdown_list_l1_l4_l1
122 | text = <<~MARKDOWN
123 | * level-1
124 | * level-4
125 | * level-3
126 | * level-2
127 | * level-1
128 | MARKDOWN
129 | expected = <<~HTML
130 |
131 | - level-1
132 | * level-4
133 |
134 | - level-3
135 | - level-2
136 |
137 |
138 | - level-1
139 |
140 | HTML
141 | actual = Kramdown::Document.new(text).to_html
142 |
143 | assert_equal(expected, actual)
144 | end
145 |
146 | def test_kramdown_list_l1_l3_l1
147 | text = <<~MARKDOWN
148 | * level-1
149 | * level-3
150 | * level-2
151 | * level-1
152 | MARKDOWN
153 | expected = <<~HTML
154 |
155 | - level-1
156 |
157 | - level-3
158 | - level-2
159 |
160 |
161 | - level-1
162 |
163 | HTML
164 | actual = Kramdown::Document.new(text).to_html
165 |
166 | assert_equal(expected, actual)
167 | end
168 | end
169 |
--------------------------------------------------------------------------------
/lib/table_of_contents/parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'table_of_contents/helper'
4 |
5 | module Jekyll
6 | module TableOfContents
7 | # Parse html contents and generate table of contents
8 | class Parser
9 | include ::Jekyll::TableOfContents::Helper
10 |
11 | def initialize(html, options = {})
12 | @doc = Nokogiri::HTML::DocumentFragment.parse(html)
13 | @configuration = Configuration.new(options)
14 | @entries = parse_content
15 | end
16 |
17 | def toc
18 | build_toc + inject_anchors_into_html
19 | end
20 |
21 | def build_toc
22 | %(<#{list_tag} id="#{@configuration.list_id}" class="#{@configuration.list_class}">\n#{build_toc_list(@entries)}#{list_tag}>)
23 | end
24 |
25 | def inject_anchors_into_html
26 | @entries.each do |entry|
27 | # NOTE: `entry[:id]` is automatically URL encoded by Nokogiri
28 | entry[:header_content].add_previous_sibling(
29 | %()
30 | )
31 | end
32 |
33 | @doc.inner_html
34 | end
35 |
36 | private
37 |
38 | # parse logic is from html-pipeline toc_filter
39 | # https://github.com/jch/html-pipeline/blob/v1.1.0/lib/html/pipeline/toc_filter.rb
40 | def parse_content
41 | headers = Hash.new(0)
42 |
43 | (@doc.css(toc_headings) - @doc.css(toc_headings_in_no_toc_section))
44 | .reject { |n| n.classes.include?(@configuration.no_toc_class) }
45 | .inject([]) do |entries, node|
46 | text = node.text
47 | id = node.attribute('id') || generate_toc_id(text)
48 |
49 | suffix_num = headers[id]
50 | headers[id] += 1
51 |
52 | entries << {
53 | id: suffix_num.zero? ? id : "#{id}-#{suffix_num}",
54 | text: CGI.escapeHTML(text),
55 | node_name: node.name,
56 | header_content: node.children.first,
57 | h_num: node.name.delete('h').to_i
58 | }
59 | end
60 | end
61 |
62 | # Returns the list items for entries
63 | def build_toc_list(entries)
64 | i = 0
65 | toc_list = +''
66 | min_h_num = entries.map { |e| e[:h_num] }.min
67 |
68 | while i < entries.count
69 | entry = entries[i]
70 | if entry[:h_num] == min_h_num
71 | # If the current entry should not be indented in the list, add the entry to the list
72 | toc_list << %(- #{entry[:text]})
73 | # If the next entry should be indented in the list, generate a sublist
74 | next_i = i + 1
75 | if next_i < entries.count && entries[next_i][:h_num] > min_h_num
76 | nest_entries = get_nest_entries(entries[next_i, entries.count], min_h_num)
77 | toc_list << %(\n<#{list_tag}#{ul_attributes}>\n#{build_toc_list(nest_entries)}#{list_tag}>\n)
78 | i += nest_entries.count
79 | end
80 | # Add the closing tag for the current entry in the list
81 | toc_list << %(
\n)
82 | elsif entry[:h_num] > min_h_num
83 | # If the current entry should be indented in the list, generate a sublist
84 | nest_entries = get_nest_entries(entries[i, entries.count], min_h_num)
85 | toc_list << build_toc_list(nest_entries)
86 | i += nest_entries.count - 1
87 | end
88 | i += 1
89 | end
90 |
91 | toc_list
92 | end
93 |
94 | # Returns the entries in a nested list
95 | # The nested list starts at the first entry in entries (inclusive)
96 | # The nested list ends at the first entry in entries with depth min_h_num or greater (exclusive)
97 | def get_nest_entries(entries, min_h_num)
98 | entries.inject([]) do |nest_entries, entry|
99 | break nest_entries if entry[:h_num] == min_h_num
100 |
101 | nest_entries << entry
102 | end
103 | end
104 |
105 | def toc_headings
106 | @configuration.toc_levels.map { |level| "h#{level}" }.join(',')
107 | end
108 |
109 | def toc_headings_in_no_toc_section
110 | if @configuration.no_toc_section_class.is_a?(Array)
111 | @configuration.no_toc_section_class.map { |cls| toc_headings_within(cls) }.join(',')
112 | else
113 | toc_headings_within(@configuration.no_toc_section_class)
114 | end
115 | end
116 |
117 | def toc_headings_within(class_name)
118 | @configuration.toc_levels.map { |level| ".#{class_name} h#{level}" }.join(',')
119 | end
120 |
121 | def ul_attributes
122 | @ul_attributes ||= @configuration.sublist_class.empty? ? '' : %( class="#{@configuration.sublist_class}")
123 | end
124 |
125 | def list_tag
126 | @list_tag ||= @configuration.ordered_list ? 'ol' : 'ul'
127 | end
128 | end
129 | end
130 | end
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jekyll-toc
2 |
3 | 
4 | [](https://badge.fury.io/rb/jekyll-toc)
5 | [](https://codeclimate.com/github/toshimaru/jekyll-toc)
6 | [](https://codeclimate.com/github/toshimaru/jekyll-toc/test_coverage)
7 |
8 | ## Table of Contents
9 |
10 | - [Installation](#installation)
11 | - [Usage](#usage)
12 | - [Basic Usage](#basic-usage)
13 | - [Advanced Usage](#advanced-usage)
14 | - [Generated HTML](#generated-html)
15 | - [Customization](#customization)
16 | - [Default Configuration](#default-configuration)
17 | - [TOC levels](#toc-levels)
18 | - [Enable TOC by default](#enable-toc-by-default)
19 | - [Skip TOC](#skip-toc)
20 | - [Skip TOC Sectionally](#skip-toc-sectionally)
21 | - [CSS Styling](#css-styling)
22 | - [Custom CSS Class and ID](#custom-css-class-and-id)
23 | - [Using Unordered/Ordered lists](#using-unorderedordered-lists)
24 | - [Alternative Tools](#alternative-tools)
25 |
26 | ## Installation
27 |
28 | Add jekyll-toc plugin in your site's `Gemfile`, and run `bundle install`.
29 |
30 | ```ruby
31 | gem 'jekyll-toc'
32 | ```
33 |
34 | Add jekyll-toc to the `gems:` section in your site's `_config.yml`.
35 |
36 | ```yml
37 | plugins:
38 | - jekyll-toc
39 | ```
40 |
41 | Set `toc: true` in posts for which you want the TOC to appear.
42 |
43 | ```yml
44 | ---
45 | layout: post
46 | title: "Welcome to Jekyll!"
47 | toc: true
48 | ---
49 | ```
50 |
51 | ## Usage
52 |
53 | There are three Liquid filters, which can be applied to HTML content,
54 | e.g. the Liquid variable `content` available in Jekyll's templates.
55 |
56 | ### Basic Usage
57 |
58 | #### `toc` filter
59 |
60 | Add the `toc` filter to your site's `{{ content }}` (e.g. `_layouts/post.html`).
61 |
62 | ```liquid
63 | {{ content | toc }}
64 | ```
65 |
66 | This filter places the TOC directly above the content.
67 |
68 | ### Advanced Usage
69 |
70 | If you'd like separated TOC and content, you can use `{% toc %}` tag (or `toc_only` filter) and `inject_anchors` filter.
71 |
72 | #### `{% toc %}` tag / `toc_only` filter
73 |
74 | Generates the TOC itself as described [below](#generated-html).
75 | Mostly useful in cases where the TOC should _not_ be placed immediately
76 | above the content but at some other place of the page, i.e. an aside.
77 |
78 | ```html
79 |
80 |
81 | {% toc %}
82 |
83 |
84 | {{ content }}
85 |
86 |
87 | ```
88 |
89 | :warning: **`{% toc %}` Tag Limitation**
90 |
91 | `{% toc %}` works only for [Jekyll Posts](https://jekyllrb.com/docs/step-by-step/08-blogging/) and [Jekyll Collections](https://jekyllrb.com/docs/collections/).
92 | If you'd like to use `{% toc %}` except posts or collections, please use `toc_only` filter as described below.
93 |
94 | ```html
95 |
96 |
97 | {{ content | toc_only }}
98 |
99 |
100 | {{ content | inject_anchors }}
101 |
102 |
103 | ```
104 |
105 | #### `inject_anchors` filter
106 |
107 | Injects HTML anchors into the content without actually outputting the TOC itself.
108 | They are of the form:
109 |
110 | ```html
111 |
112 |
113 |
114 | ```
115 |
116 | This is only useful when the TOC itself should be placed at some other
117 | location with the `toc_only` filter.
118 |
119 | ## Generated HTML
120 |
121 | jekyll-toc generates an unordered list by default. The HTML output is as follows.
122 |
123 | ```html
124 |
143 | ```
144 |
145 | 
146 |
147 | ## Customization
148 |
149 | jekyll-toc is customizable on `_config.yml`.
150 |
151 | ### Default Configuration
152 |
153 | ```yml
154 | # _config.yml
155 | toc:
156 | min_level: 1
157 | max_level: 6
158 | ordered_list: false
159 | no_toc_section_class: no_toc_section
160 | list_id: toc
161 | list_class: section-nav
162 | sublist_class: ''
163 | item_class: toc-entry
164 | item_prefix: toc-
165 | ```
166 |
167 | ### TOC levels
168 |
169 | ```yml
170 | # _config.yml
171 | toc:
172 | min_level: 2 # default: 1
173 | max_level: 5 # default: 6
174 | ```
175 |
176 | The default heading range is from `` to ``.
177 |
178 | ### Enable TOC by default
179 |
180 | You can enable TOC by default with [Front Matter Defaults](https://jekyllrb.com/docs/configuration/front-matter-defaults/):
181 |
182 | ```yml
183 | # _config.yml
184 | defaults:
185 | - scope:
186 | path: ""
187 | values:
188 | toc: true
189 | ```
190 |
191 | ### Skip TOC
192 |
193 | The heading is ignored in the toc by adding `no_toc` class.
194 |
195 | ```html
196 | h1
197 | This heading is ignored in the TOC
198 | h2
199 | ```
200 |
201 | ### Skip TOC Sectionally
202 |
203 | The headings are ignored inside the element which has `no_toc_section` class.
204 |
205 | ```html
206 | h1
207 |
208 |
This heading is ignored in the TOC
209 | This heading is ignored in the TOC
210 |
211 | h4
212 | ```
213 |
214 | Which would result in only the `` & `` within the example being included in the TOC.
215 |
216 | The class can be configured on `_config.yml`:
217 |
218 | ```yml
219 | # _config.yml
220 | toc:
221 | no_toc_section_class: exclude # default: no_toc_section
222 | ```
223 |
224 | Configuring multiple classes are allowed:
225 |
226 | ```yml
227 | # _config.yml
228 | toc:
229 | no_toc_section_class:
230 | - no_toc_section
231 | - exclude
232 | - your_custom_skip_class_name
233 | ```
234 |
235 | ### CSS Styling
236 |
237 | The toc can be modified with CSS. The sample CSS is the following.
238 |
239 | ```css
240 | .section-nav {
241 | background-color: #fff;
242 | margin: 5px 0;
243 | padding: 10px 30px;
244 | border: 1px solid #e8e8e8;
245 | border-radius: 3px;
246 | }
247 | ```
248 |
249 | 
250 |
251 | Each TOC `li` entry has two CSS classes for further styling. The general `toc-entry` is applied to all `li` elements in the `ul.section-nav`.
252 |
253 | Depending on the heading level each specific entry refers to, it has a second CSS class `toc-XX`, where `XX` is the HTML heading tag name.
254 | For example, the TOC entry linking to a heading `...
` (a single `#` in Markdown) will get the CSS class `toc-h1`.
255 |
256 | ### Custom CSS Class and ID
257 |
258 | You can apply custom CSS classes to the generated `` and `- ` tags.
259 |
260 | ```yml
261 | # _config.yml
262 | toc:
263 | list_id: my-toc-id # Default: "toc"
264 | list_class: my-list-class # Default: "section-nav"
265 | sublist_class: my-sublist-class # Default: no class for sublists
266 | item_class: my-item-class # Default: "toc-entry"
267 | item_prefix: item- # Default: "toc-":
268 | ```
269 |
270 | ### Using Unordered/Ordered lists
271 |
272 | By default the table of contents will be generated as an unordered list via `` tags. This can be configured to use ordered lists instead `
`.
273 | This can be configured in `_config.yml`:
274 |
275 | ```yml
276 | # _config.yml
277 | toc:
278 | ordered_list: true # default is false
279 | ```
280 |
281 | In order to change the list type, use the [list-style-type](https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type) css property.
282 | Add a class to the `sublist_class` configuration to append it to the `ol` tags so that you can add the `list-style-type` property.
283 |
284 | Example
285 |
286 | ```yml
287 | # _config.yml
288 | toc:
289 | ordered_list: true
290 | list_class: my-list-class
291 | sublist_class: my-sublist-class
292 | ```
293 |
294 | ```css
295 | .my-list-class {
296 | list-style-type: upper-alpha;
297 | }
298 |
299 | .my-sublist-class: {
300 | list-style-type: lower-alpha;
301 | }
302 | ```
303 |
304 | This will produce:
305 |
306 | 
307 |
308 | ## Alternative Tools
309 |
310 | - Adding anchor to headings
311 | - [AnchorJS](https://www.bryanbraun.com/anchorjs/)
312 | - Generating TOC for kramdown content
313 | - [Automatic “Table of Contents” Generation](https://kramdown.gettalong.org/converter/html.html#toc) (See also. [Create Table of Contents in kramdown](https://blog.toshima.ru/2020/05/22/kramdown-toc))
314 |
--------------------------------------------------------------------------------
/test/parser/test_various_toc_html.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class TestVariousTocHtml < Minitest::Test
6 | TEST_HTML = <<~HTML
7 | h1
8 | h3
9 | h6
10 | HTML
11 |
12 | def test_nested_toc
13 | parser = Jekyll::TableOfContents::Parser.new(TEST_HTML)
14 | expected = <<~HTML.chomp
15 |
26 | HTML
27 |
28 | assert_equal(expected, parser.build_toc)
29 | end
30 |
31 | def test_nested_toc_with_min_and_max
32 | parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, 'min_level' => 2, 'max_level' => 5)
33 | expected = <<~HTML.chomp
34 |
37 | HTML
38 |
39 | assert_equal(expected, parser.build_toc)
40 | end
41 |
42 | def test_complex_nested_toc
43 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
44 | h1
45 | h3
46 | h2
47 | h6
48 | HTML
49 | expected = <<~HTML.chomp
50 |
51 | - h1
52 |
53 | - h3
54 | - h2
55 |
58 |
59 |
60 |
61 |
62 | HTML
63 |
64 | assert_equal(expected, parser.build_toc)
65 | end
66 |
67 | def test_decremental_headings1
68 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
69 | h6
70 | h5
71 | h4
72 | h3
73 | h2
74 | h1
75 | HTML
76 | expected = <<~HTML.chomp
77 |
85 | HTML
86 |
87 | assert_equal(expected, parser.build_toc)
88 | end
89 |
90 | def test_decremental_headings2
91 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
92 | h1
93 | h3
94 | h2
95 | h4
96 | h5
97 | HTML
98 | expected = <<~HTML.chomp
99 |
100 | - h1
101 |
102 | - h3
103 | - h2
104 |
105 | - h4
106 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | HTML
116 |
117 | assert_equal(expected, parser.build_toc)
118 | end
119 |
120 | def test_no_toc
121 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
122 | h1
123 | no_toc h1
124 | h2
125 | no_toc h2
126 | h3
127 | no_toc h3
128 | h4
129 | no_toc h4
130 | HTML
131 | expected = <<~HTML.chomp
132 |
133 | - h1
134 |
135 | - h2
136 |
137 | - h3
138 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | HTML
148 |
149 | assert_equal(expected, parser.build_toc)
150 | end
151 |
152 | def test_japanese_toc
153 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
154 | あ
155 | い
156 | う
157 | HTML
158 | expected = <<~HTML.chomp
159 |
160 | - あ
161 |
162 | - い
163 |
166 |
167 |
168 |
169 |
170 | HTML
171 |
172 | assert_equal(expected, parser.build_toc)
173 | html_with_anchors = parser.inject_anchors_into_html
174 |
175 | assert_match(%r{あ}, html_with_anchors)
176 | assert_match(%r{い}, html_with_anchors)
177 | assert_match(%r{う}, html_with_anchors)
178 | end
179 |
180 | # ref. https://github.com/toshimaru/jekyll-toc/issues/45
181 | def test_angle_bracket
182 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
183 | h1
184 | <base href>
185 | & < >
186 | HTML
187 | expected = <<~HTML.chomp
188 |
193 | HTML
194 |
195 | assert_equal(expected, parser.build_toc)
196 | end
197 |
198 | def test_tags_inside_heading
199 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
200 | h2
201 | h2
202 | HTML
203 | expected = <<~HTML.chomp
204 |
208 | HTML
209 |
210 | assert_equal(expected, parser.build_toc)
211 | end
212 |
213 | def test_nested_toc_with_no_toc_section_class
214 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
215 | h1
216 |
217 |
h2
218 |
219 | h3
220 | h6
221 | HTML
222 | expected = <<~HTML.chomp
223 |
224 | - h1
225 |
226 | - h3
227 |
230 |
231 |
232 |
233 |
234 | HTML
235 | assert_equal(expected, parser.build_toc)
236 |
237 | html = parser.inject_anchors_into_html
238 |
239 | assert_match(%r{.+
}m, html)
240 | assert_match(%r{.+
}m, html)
241 | assert_match(%r{.+
}m, html)
242 | assert_includes(html, 'h2
')
243 | end
244 |
245 | def test_nested_toc_with_no_toc_section_class_option
246 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML, 'no_toc_section_class' => 'exclude')
247 | h1
248 |
249 |
h2
250 |
251 | h3
252 |
253 |
h4
254 | h5
255 |
256 | h6
257 | HTML
258 | expected = <<~HTML.chomp
259 |
260 | - h1
261 |
262 | - h3
263 |
266 |
267 |
268 |
269 |
270 | HTML
271 | assert_equal(expected, parser.build_toc)
272 |
273 | html = parser.inject_anchors_into_html
274 |
275 | assert_match(%r{.+
}m, html)
276 | assert_match(%r{.+
}m, html)
277 | assert_match(%r{.+
}m, html)
278 | assert_includes(html, 'h2
')
279 | assert_includes(html, 'h4
')
280 | assert_includes(html, 'h5
')
281 | end
282 |
283 | def test_multiple_no_toc_section_classes
284 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML, 'no_toc_section_class' => ['no_toc_section', 'exclude'])
285 | h1
286 |
287 |
h2
288 |
289 | h3
290 |
291 |
h4
292 | h5
293 |
294 | h6
295 | HTML
296 | expected = <<~HTML.chomp
297 |
298 | - h1
299 |
300 | - h3
301 |
304 |
305 |
306 |
307 |
308 | HTML
309 | assert_equal(expected, parser.build_toc)
310 |
311 | html = parser.inject_anchors_into_html
312 |
313 | assert_match(%r{.+
}m, html)
314 | assert_match(%r{.+
}m, html)
315 | assert_match(%r{.+
}m, html)
316 | assert_includes(html, 'h2
')
317 | assert_includes(html, 'h4
')
318 | assert_includes(html, 'h5
')
319 | end
320 |
321 | def test_toc_with_explicit_id
322 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
323 | h1
324 | h2
325 | h3
326 | HTML
327 | expected = <<~HTML.chomp
328 |
329 | - h1
330 | - h2
331 | - h3
332 |
333 | HTML
334 | assert_equal(expected, parser.build_toc)
335 |
336 | html = parser.inject_anchors_into_html
337 |
338 | assert_includes(html, %())
339 | assert_includes(html, %())
340 | assert_includes(html, %())
341 | end
342 |
343 | def test_anchor_is_uniq
344 | parser = Jekyll::TableOfContents::Parser.new(<<~HTML)
345 | h1
346 | h1
347 | h1
348 | HTML
349 | expected = <<~HTML.chomp
350 |
351 | - h1
352 | - h1
353 | - h1
354 |
355 | HTML
356 |
357 | assert_equal(expected, parser.build_toc)
358 | end
359 |
360 | def test_custom_css_classes
361 | parser = Jekyll::TableOfContents::Parser.new(
362 | TEST_HTML,
363 | 'item_class' => 'custom-item', 'list_id' => 'custom-toc-id', 'list_class' => 'custom-list', 'sublist_class' => 'custom-sublist', 'item_prefix' => 'custom-prefix-'
364 | )
365 | expected = <<~HTML.chomp
366 |
367 | - h1
368 |
369 | - h3
370 |
373 |
374 |
375 |
376 |
377 | HTML
378 |
379 | assert_equal(expected, parser.build_toc)
380 | end
381 | end
382 |
--------------------------------------------------------------------------------