Skip to content

Commit

Permalink
Merge pull request #6 from torbjoernk/feature/separate-filters
Browse files Browse the repository at this point in the history
Add new filters `toc_only` and `inject_anchors` to split up existing filter
  • Loading branch information
toshimaru committed Mar 22, 2016
2 parents 0f65775 + 8e0ba01 commit 0eb8b51
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 37 deletions.
76 changes: 65 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ gems:
- jekyll-toc
```
Add `toc` filter to your site's `{{ content }}` (e.g. `_layouts/post.html`).

```
{{ content | toc }}
```
Set `toc: true` in your posts.

```yml
Expand All @@ -35,17 +29,51 @@ toc: true
---
```

There are three Liquid filters available now, which all should be applied
to some HTML content, e.g. the Liquid variable `content` available in
Jekyll's templates.

1. `toc_only`
Generates the TOC itself as described [below](#generated-table-of-contents-html).
Mostly useful in cases where the TOC should _not_ be placed immediately
above the content but at some other place of the page, i.e. an aside.

2. `inject_anchors`
Injects HTML anchors into the content without actually outputing the
TOC itself.
They are of the form:

```html
<a id="heading11" class="anchor" href="#heading1-1" aria-hidden="true">
<span class="octicon octicon-link"></span>
</a>
```

This is only useful when the TOC itself should be placed at some other
location with the `toc_only` filter.

3. `toc`
This is the concatenation of the two above, where the TOC is placed
directly above the content.

Add `toc` filter to your site's `{{ content }}` (e.g. `_layouts/post.html`).

```
{{ content | toc }}
```
## Generated Table of Contents HTML
jekyll-toc generates Unordered List. The final output is as follows.
```html
<ul class="section-nav">
<li><a href="#heading1">Heading.1</a></li>
<li><a href="#heading2-1">Heading.2-1</a></li>
<li><a href="#heading2-2">Heading.2-2</a></li>
<li><a href="#heading3">Heading.3</a></li>
<li><a href="#heading2-3">Heading.2-3</a></li>
<li class="toc-entry toc-h1"><a href="#heading1">Heading.1</a></li>
<li class="toc-entry toc-h2"><a href="#heading2-1">Heading.2-1</a></li>
<li class="toc-entry toc-h2"><a href="#heading2-2">Heading.2-2</a></li>
<li class="toc-entry toc-h3"><a href="#heading3">Heading.3</a></li>
<li class="toc-entry toc-h2"><a href="#heading2-3">Heading.2-3</a></li>
</ul>
```

Expand All @@ -68,3 +96,29 @@ The toc can be modified with CSS. The sample CSS is the following.
```

![screenshot](https://cloud.githubusercontent.com/assets/803398/5723662/f0bc84c8-9b88-11e4-986c-90608ca88184.png)

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`.
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.

For example, the TOC entry linking to a heading `<h1>...</h1>` (a single
`#` in Markdown) will get the CSS class `toc-h1`.

That way, one can tune the depth of the TOC displayed on the site.
The following CSS will display only the first two heading levels and hides
all other links:

```css
.toc-entry.toc-h1,
.toc-entry.toc-h2
{}
.toc-entry.toc-h3,
.toc-entry.toc-h4,
.toc-entry.toc-h5,
.toc-entry.toc-h6
{
display: none;
}
```

2 changes: 1 addition & 1 deletion jekyll-toc.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
spec.version = JekyllToc::VERSION
spec.summary = "Jekyll Table of Contents plugin"
spec.description = "A liquid filter plugin for Jekyll which generates a table of contents."
spec.authors = ["Toshimaru"]
spec.authors = ["Toshimaru", 'torbjoernk']
spec.email = '[email protected]'
spec.files = `git ls-files -z`.split("\x0")
spec.homepage = 'https://github.com/toshimaru/jekyll-toc'
Expand Down
103 changes: 79 additions & 24 deletions lib/jekyll-toc.rb
Original file line number Diff line number Diff line change
@@ -1,35 +1,90 @@
require 'nokogiri'

module Jekyll
# parse logic is from html-pipeline toc_filter
# https://github.com/jch/html-pipeline/blob/v1.1.0/lib/html/pipeline/toc_filter.rb
module TableOfContents
PUNCTUATION_REGEXP = RUBY_VERSION > '1.9' ? /[^\p{Word}\- ]/u : /[^\w\- ]/

class Parser
attr_reader :doc

def initialize(html)
@doc = Nokogiri::HTML::DocumentFragment.parse(html)
@entries = parse_content
end

def build_toc
toc = %Q{<ul class="section-nav">\n}

@entries.each do |entry|
toc << %Q{<li class="toc-entry toc-#{entry[:node_name]}"><a href="##{entry[:id]}#{entry[:uniq]}">#{entry[:text]}</a></li>\n}
end

toc << '</ul>'
end

def inject_anchors_into_html
@entries.each do |entry|
entry[:content_node].add_previous_sibling(%Q{<a id="#{entry[:id]}#{entry[:uniq]}" class="anchor" href="##{entry[:id]}#{entry[:uniq]}" aria-hidden="true"><span class="octicon octicon-link"></span></a>})
end

@doc.inner_html
end

def toc
build_toc + inject_anchors_into_html
end

# parse logic is from html-pipeline toc_filter
# https://github.com/jch/html-pipeline/blob/v1.1.0/lib/html/pipeline/toc_filter.rb
private
def parse_content
entries = []
headers = Hash.new(0)

@doc.css('h1, h2, h3, h4, h5, h6').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
if header_content = node.children.first
entries << {
id: id,
uniq: uniq,
text: text,
node_name: node.name,
content_node: header_content
}
end
end

entries
end
end
end

module TableOfContentsFilter
PUNCTUATION_REGEXP = RUBY_VERSION > "1.9" ? /[^\p{Word}\- ]/u : /[^\w\- ]/
def toc_only(html)
page = @context.registers[:page]
return html unless page['toc']

Jekyll::TableOfContents::Parser.new(html).build_toc
end

def inject_anchors(html)
page = @context.registers[:page]
return html unless page['toc']

Jekyll::TableOfContents::Parser.new(html).inject_anchors_into_html
end

def toc(html)
page = @context.registers[:page]
return html unless page["toc"]

toc = ""
doc = Nokogiri::HTML::DocumentFragment.parse(html)
headers = Hash.new(0)

doc.css('h1, h2, h3, h4, h5, h6').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
if header_content = node.children.first
toc << %Q{<li><a href="##{id}#{uniq}">#{text}</a></li>\n}
header_content.add_previous_sibling(%Q{<a id="#{id}#{uniq}" class="anchor" href="##{id}#{uniq}" aria-hidden="true"><span class="octicon octicon-link"></span></a>})
end
end
toc = %Q{<ul class="section-nav">\n#{toc}</ul>} unless toc.empty?
return html unless page['toc']

toc + doc.inner_html
Jekyll::TableOfContents::Parser.new(html).toc
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module JekyllToc
VERSION = "0.0.4"
VERSION = '0.1.0'
end
16 changes: 16 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
require 'jekyll'
require 'minitest/autorun'

SIMPLE_HTML = <<EOL
<h1>Simple H1</h1>
<h2>Simple H2</h2>
<h3>Simple H3</h3>
<h4>Simple H4</h4>
<h5>Simple H5</h5>
<h6>Simple H6</h6>
EOL

module TestHelpers
def read_html_and_create_parser
@parser = Jekyll::TableOfContents::Parser.new(SIMPLE_HTML)
assert_match /Simple H1/, @parser.doc.inner_html
end
end
23 changes: 23 additions & 0 deletions test/test_inject_anchors_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'test_helper'
require_relative '../lib/jekyll-toc'


class TestTOCOnlyFilter < 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 /<a id="simple\-h1" class="anchor" href="#simple\-h1" aria\-hidden="true"><span.*span><\/a>Simple H1/, html
end

def test_does_not_inject_toc
html = @parser.inject_anchors_into_html

assert_nil /<ul class="section-nav">/ =~ html
end
end
23 changes: 23 additions & 0 deletions test/test_toc_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'test_helper'
require_relative '../lib/jekyll-toc'


class TestTOCFilter < Minitest::Test
include TestHelpers

def setup
read_html_and_create_parser
end

def test_injects_anchors
html = @parser.toc

assert_match /<a id="simple\-h1" class="anchor" href="#simple\-h1" aria\-hidden="true"><span.*span><\/a>Simple H1/, html
end

def test_injects_toc_container
html = @parser.toc

assert_match /<ul class="section-nav">/, html
end
end
23 changes: 23 additions & 0 deletions test/test_toc_only_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'test_helper'
require_relative '../lib/jekyll-toc'


class TestTOCOnlyFilter < Minitest::Test
include TestHelpers

def setup
read_html_and_create_parser
end

def test_injects_toc_container
html = @parser.build_toc

assert_match /<ul class="section-nav">/, html
end

def test_does_not_return_content
html = @parser.build_toc

assert_nil /<h1>Simple H1<\/h1>/ =~ html
end
end

0 comments on commit 0eb8b51

Please sign in to comment.