Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 109 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
- [CSS Styling](#css-styling)
- [Custom CSS Class and ID](#custom-css-class-and-id)
- [Using Unordered/Ordered lists](#using-unorderedordered-lists)
- [Using divs as list elements](#using-divs-as-list-elements)
- [Using a flat list](#using-a-flat-list)
- [Alternative Tools](#alternative-tools)

## Installation
Expand Down Expand Up @@ -162,6 +164,8 @@ toc:
sublist_class: ''
item_class: toc-entry
item_prefix: toc-
div_list: false
flat_list: false
```

### TOC levels
Expand Down Expand Up @@ -296,7 +300,7 @@ toc:
list-style-type: upper-alpha;
}

.my-sublist-class: {
.my-sublist-class {
list-style-type: lower-alpha;
}
```
Expand All @@ -305,6 +309,110 @@ This will produce:

![screenshot](https://user-images.githubusercontent.com/7675276/85813980-a0ea5a80-b719-11ea-9458-ccf9b86a778b.png)

### Using divs as list elements

By default, the table of contents is generated using `<ul>`, `<ol>`, and `<li>` tags. If you prefer to use `<div>` elements instead (for custom styling or accessibility reasons), you can enable this by setting the `div_list` option in your `_config.yml`:

```yml
# _config.yml
toc:
div_list: true # default is false
```

When `div_list` is set to `true`, the TOC will be rendered using `<div>` elements for both the list container and each entry, instead of `<ul>`, `<ol>`, and `<li>`. You can still use the `list_class`, `sublist_class`, and `item_class` options to add custom CSS classes for styling:

```yml
# _config.yml
toc:
div_list: true
list_class: my-list-class
sublist_class: my-sublist-class
item_class: my-item-class
```

Example CSS for styling the TOC with `<div>` elements:

```css
.my-list-class {
/* Styles for the TOC container */
margin: 10px 0;
}

.my-item-class {
/* Styles for each TOC entry */
padding: 4px 0;
}

.my-sublist-class {
/* Styles for nested TOC containers */
margin-left: 20px;
}
```

This will produce a TOC structure like:

```html
<div id="toc" class="my-list-class">
<div class="my-item-class toc-h1"><a href="#heading1">Heading.1</a>
<div class="my-sublist-class">
<div class="my-item-class toc-h2"><a href="#heading1-1">Heading.1-1</a></div>
<div class="my-item-class toc-h2"><a href="#heading1-2">Heading.1-2</a></div>
</div>
</div>
<div class="my-item-class toc-h1"><a href="#heading2">Heading.2</a></div>
</div>
```

Use this option if you want more flexibility in styling or need to avoid list semantics for accessibility or design reasons.

### Using a flat list

By default, the table of contents is generated as a nested structure that reflects the hierarchy of headings in your content. If you prefer a flat list with no nesting (where all TOC entries appear at the same level regardless of their heading level), you can enable this by setting the `flat_list` option in your `_config.yml`:

```yml
# _config.yml
toc:
flat_list: true # default is false
```

When `flat_list` is set to `true`, all TOC entries will be rendered at the same level without any nesting, while still retaining the CSS classes that indicate their heading level. This is useful when you want to style headings differently based on their level but prefer a simplified, non-nested list structure.

The flat list option works with both standard lists (`<ul>`, `<ol>`) and div-based lists (when `div_list` is set to `true`).

Example with standard lists:

```html
<ul id="toc" class="section-nav">
<li class="toc-entry toc-h1"><a href="#heading1">Heading.1</a></li>
<li class="toc-entry toc-h2"><a href="#heading1-1">Heading.1-1</a></li>
<li class="toc-entry toc-h2"><a href="#heading1-2">Heading.1-2</a></li>
<li class="toc-entry toc-h1"><a href="#heading2">Heading.2</a></li>
<li class="toc-entry toc-h2"><a href="#heading2-1">Heading.2-1</a></li>
<li class="toc-entry toc-h3"><a href="#heading2-1-1">Heading.2-1-1</a></li>
</ul>
```

Example with div-based lists:

```html
<div id="toc" class="section-nav">
<div class="toc-entry toc-h1"><a href="#heading1">Heading.1</a></div>
<div class="toc-entry toc-h2"><a href="#heading1-1">Heading.1-1</a></div>
<div class="toc-entry toc-h2"><a href="#heading1-2">Heading.1-2</a></div>
<div class="toc-entry toc-h1"><a href="#heading2">Heading.2</a></div>
<div class="toc-entry toc-h2"><a href="#heading2-1">Heading.2-1</a></div>
<div class="toc-entry toc-h3"><a href="#heading2-1-1">Heading.2-1-1</a></div>
</div>
```

Use this option when you want a simplified TOC structure but still want to style entries differently based on their heading level using CSS. For example:

```css
.toc-h1 { font-weight: bold; font-size: 1.2em; }
.toc-h2 { font-weight: normal; font-size: 1.1em; }
.toc-h3 { font-style: italic; font-size: 1em; }
```

## Alternative Tools

- Adding anchor to headings
Expand Down
8 changes: 6 additions & 2 deletions lib/table_of_contents/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module TableOfContents
# jekyll-toc configuration class
class Configuration
attr_reader :toc_levels, :no_toc_class, :ordered_list, :no_toc_section_class,
:list_id, :list_class, :sublist_class, :item_class, :item_prefix
:list_id, :list_class, :sublist_class, :item_class, :item_prefix, :div_list, :flat_list

DEFAULT_CONFIG = {
'min_level' => 1,
Expand All @@ -16,7 +16,9 @@ class Configuration
'list_class' => 'section-nav',
'sublist_class' => '',
'item_class' => 'toc-entry',
'item_prefix' => 'toc-'
'item_prefix' => 'toc-',
'div_list' => false,
'flat_list' => false
}.freeze

def initialize(options)
Expand All @@ -31,6 +33,8 @@ def initialize(options)
@sublist_class = options['sublist_class']
@item_class = options['item_class']
@item_prefix = options['item_prefix']
@div_list = options['div_list']
@flat_list = options['flat_list']
end

private
Expand Down
40 changes: 33 additions & 7 deletions lib/table_of_contents/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ def parse_content

# Returns the list items for entries
def build_toc_list(entries)
if @configuration.flat_list
build_flat_toc_list(entries)
else
build_nested_toc_list(entries)
end
end

# Returns the list items for entries in a flat structure
def build_flat_toc_list(entries)
toc_list = +''

entries.each do |entry|
toc_list << %(<#{list_parent_tag} class="#{@configuration.item_class} #{@configuration.item_prefix}#{entry[:node_name]}"><a href="##{entry[:id]}">#{entry[:text]}</a></#{list_parent_tag}>\n)
end

toc_list
end

# Returns the list items for entries in a nested structure
def build_nested_toc_list(entries)
i = 0
toc_list = +''
min_h_num = entries.map { |e| e[:h_num] }.min
Expand All @@ -69,20 +89,18 @@ def build_toc_list(entries)
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
toc_list << %(<#{list_parent_tag} class="#{@configuration.item_class} #{@configuration.item_prefix}#{entry[:node_name]}"><a href="##{entry[:id]}">#{entry[:text]}</a>)
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)
toc_list << %(\n<#{list_tag}#{ul_attributes}>\n#{build_nested_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)
toc_list << %(</#{list_parent_tag}>\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)
toc_list << build_nested_toc_list(nest_entries)
i += nest_entries.count - 1
end
i += 1
Expand Down Expand Up @@ -122,8 +140,16 @@ def ul_attributes
@ul_attributes ||= @configuration.sublist_class.empty? ? '' : %( class="#{@configuration.sublist_class}")
end

def list_parent_tag
@list_parent_tag ||= @configuration.div_list ? 'div' : 'li'
end

def list_tag
@list_tag ||= @configuration.ordered_list ? 'ol' : 'ul'
@list_tag ||= if @configuration.div_list
'div'
else
(@configuration.ordered_list ? 'ol' : 'ul')
end
end
end
end
Expand Down
51 changes: 51 additions & 0 deletions test/parser/test_div_list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'test_helper'

class TestDivList < Minitest::Test
TEST_HTML = '<h1>h1</h1><h2>h2</h2><h3>h3</h3>'

def test_basic_div_toc
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, 'div_list' => true)
html = parser.build_toc

assert_match(/^<div id="toc" class="section-nav">/, html)
assert_match(%r{<div class="toc-entry toc-h1"><a href="#h1">h1</a>}, html)
assert_match(%r{<div class="toc-entry toc-h2"><a href="#h2">h2</a>}, html)
assert_match(%r{<div class="toc-entry toc-h3"><a href="#h3">h3</a></div>}, html)
refute_match(/<ul/, html)
refute_match(/<ol/, html)
end

def test_div_toc_with_custom_classes
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, {
'div_list' => true,
'list_id' => 'custom-toc-id',
'list_class' => 'custom-list',
'sublist_class' => 'custom-sublist',
'item_class' => 'custom-item',
'item_prefix' => 'custom-prefix-'
})
html = parser.build_toc

assert_match(/^<div id="custom-toc-id" class="custom-list">/, html)
assert_match(%r{<div class="custom-item custom-prefix-h1"><a href="#h1">h1</a>}, html)
assert_match(%r{<div class="custom-item custom-prefix-h2"><a href="#h2">h2</a>}, html)
assert_match(%r{<div class="custom-item custom-prefix-h3"><a href="#h3">h3</a></div>}, html)
assert_match(/<div class="custom-sublist">/, html)
refute_match(/<ul/, html)
refute_match(/<ol/, html)
end

def test_div_toc_ignores_ordered_list
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, {
'div_list' => true,
'ordered_list' => true
})
html = parser.build_toc

assert_match(/^<div id="toc" class="section-nav">/, html)
refute_match(/<ol/, html)
refute_match(/<ul/, html)
end
end
100 changes: 100 additions & 0 deletions test/parser/test_flat_list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

require 'test_helper'

class TestFlatList < Minitest::Test
TEST_HTML = '<h1>h1</h1><h2>h2</h2><h3>h3</h3>'

def test_basic_flat_toc
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, 'flat_list' => true)
html = parser.build_toc

assert_match(/^<ul id="toc" class="section-nav">/, html)
assert_match(%r{<li class="toc-entry toc-h1"><a href="#h1">h1</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h2"><a href="#h2">h2</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h3"><a href="#h3">h3</a></li>}, html)

# Make sure there's no nested structure
refute_match(%r{<ul>.+</ul>}m, html)
refute_match(/<li>.+<ul>/m, html)
refute_match(%r{</ul>.+</li>}m, html)
end

def test_flat_toc_with_custom_classes
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, {
'flat_list' => true,
'list_id' => 'custom-toc-id',
'list_class' => 'custom-list',
'item_class' => 'custom-item',
'item_prefix' => 'custom-prefix-'
})
html = parser.build_toc

assert_match(/^<ul id="custom-toc-id" class="custom-list">/, html)
assert_match(%r{<li class="custom-item custom-prefix-h1"><a href="#h1">h1</a></li>}, html)
assert_match(%r{<li class="custom-item custom-prefix-h2"><a href="#h2">h2</a></li>}, html)
assert_match(%r{<li class="custom-item custom-prefix-h3"><a href="#h3">h3</a></li>}, html)

# Make sure there's no nested structure
refute_match(%r{<ul>.+</ul>}m, html)
refute_match(/<li>.+<ul>/m, html)
refute_match(%r{</ul>.+</li>}m, html)
end

def test_flat_toc_works_with_ordered_list
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, {
'flat_list' => true,
'ordered_list' => true
})
html = parser.build_toc

assert_match(/^<ol id="toc" class="section-nav">/, html)
assert_match(%r{<li class="toc-entry toc-h1"><a href="#h1">h1</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h2"><a href="#h2">h2</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h3"><a href="#h3">h3</a></li>}, html)

# Make sure there's no nested structure
refute_match(%r{<ol>.+</ol>}m, html)
refute_match(/<li>.+<ol>/m, html)
refute_match(%r{</ol>.+</li>}m, html)
end

def test_flat_toc_with_div_list
parser = Jekyll::TableOfContents::Parser.new(TEST_HTML, {
'flat_list' => true,
'div_list' => true
})
html = parser.build_toc

assert_match(/^<div id="toc" class="section-nav">/, html)
assert_match(%r{<div class="toc-entry toc-h1"><a href="#h1">h1</a></div>}, html)
assert_match(%r{<div class="toc-entry toc-h2"><a href="#h2">h2</a></div>}, html)
assert_match(%r{<div class="toc-entry toc-h3"><a href="#h3">h3</a></div>}, html)

# Make sure there's no nested structure
refute_match(%r{<div>.+</div>}m, html)
refute_match(/<div>.+<div class="toc-entry/m, html)
end

def test_complex_flat_toc
parser = Jekyll::TableOfContents::Parser.new(<<~HTML, 'flat_list' => true)
<h1>h1</h1>
<h3>h3</h3>
<h2>h2</h2>
<h6>h6</h6>
HTML
html = parser.build_toc

# All headings should be at the same level in flat list mode
assert_match(/^<ul id="toc" class="section-nav">/, html)
assert_match(%r{<li class="toc-entry toc-h1"><a href="#h1">h1</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h3"><a href="#h3">h3</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h2"><a href="#h2">h2</a></li>}, html)
assert_match(%r{<li class="toc-entry toc-h6"><a href="#h6">h6</a></li>}, html)

# Make sure there's no nested structure
refute_match(%r{<ul>.+</ul>}m, html)
refute_match(/<li>.+<ul>/m, html)
refute_match(%r{</ul>.+</li>}m, html)
end
end
Loading