Skip to content

Commit c0f78e7

Browse files
committed
Merge pull request #75 from simeonwillbanks/generated-table-of-contents
Generate Table of Contents
2 parents 2a3063c + f4549c6 commit c0f78e7

File tree

3 files changed

+100
-16
lines changed

3 files changed

+100
-16
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ filter.call
9696
* `SanitizationFilter` - whitelist sanitize user markup
9797
* `SyntaxHighlightFilter` - [code syntax highlighter](#syntax-highlighting)
9898
* `TextileFilter` - convert textile to html
99-
* `TableOfContentsFilter` - anchor headings with name attributes
99+
* `TableOfContentsFilter` - anchor headings with name attributes and generate Table of Contents html unordered list linking headings
100100

101101
## Syntax highlighting
102102

@@ -127,7 +127,7 @@ context = {
127127
# related features.
128128
SimplePipeline = Pipeline.new [
129129
SanitizationFilter,
130-
TableOfContentsFilter, # add 'name' anchors to all headers
130+
TableOfContentsFilter, # add 'name' anchors to all headers and generate toc list
131131
CamoFilter,
132132
ImageMaxWidthFilter,
133133
SyntaxHighlightFilter,

lib/html/pipeline/toc_filter.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
11
module HTML
22
class Pipeline
33
# HTML filter that adds a 'name' attribute to all headers
4-
# in a document, so they can be accessed from a table of contents
4+
# in a document, so they can be accessed from a table of contents.
55
#
6-
# TODO: besides adding the name attribute, we should get around to
7-
# eventually generating the Table of Contents itself, with links
8-
# to each header
6+
# Generates the Table of Contents, with links to each header.
7+
#
8+
# Examples
9+
#
10+
# TocPipeline =
11+
# HTML::Pipeline.new [
12+
# HTML::Pipeline::TableOfContentsFilter
13+
# ]
14+
# # => #<HTML::Pipeline:0x007fc13c4528d8...>
15+
# orig = %(<h1>Ice cube</h1><p>is not for the pop chart</p>)
16+
# # => "<h1>Ice cube</h1><p>is not for the pop chart</p>"
17+
# result = {}
18+
# # => {}
19+
# TocPipeline.call(orig, {}, result)
20+
# # => {:toc=> ...}
21+
# result[:toc]
22+
# # => "<ul class=\"section-nav\">\n<li><a href=\"#ice-cube\">...</li><ul>"
23+
# result[:output].to_s
24+
# # => "<h1>\n<a name=\"ice-cube\" class=\"anchor\" href=\"#ice-cube\">..."
925
class TableOfContentsFilter < Filter
1026
PUNCTUATION_REGEXP = RUBY_VERSION > "1.9" ? /[^\p{Word}\- ]/u : /[^\w\- ]/
1127

1228
def call
29+
result[:toc] = ""
30+
1331
headers = Hash.new(0)
1432
doc.css('h1, h2, h3, h4, h5, h6').each do |node|
15-
name = node.text.downcase
33+
text = node.text
34+
name = text.downcase
1635
name.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
1736
name.gsub!(' ', '-') # replace spaces with dash
1837

1938
uniq = (headers[name] > 0) ? "-#{headers[name]}" : ''
2039
headers[name] += 1
2140
if header_content = node.children.first
41+
result[:toc] << %Q{<li><a href="##{name}#{uniq}">#{text}</a></li>\n}
2242
header_content.add_previous_sibling(%Q{<a name="#{name}#{uniq}" class="anchor" href="##{name}#{uniq}"><span class="octicon octicon-link"></span></a>})
2343
end
2444
end
45+
result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty?
2546
doc
2647
end
2748
end

test/html/pipeline/toc_filter_test.rb

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,27 @@
44
class HTML::Pipeline::TableOfContentsFilterTest < Test::Unit::TestCase
55
TocFilter = HTML::Pipeline::TableOfContentsFilter
66

7+
TocPipeline =
8+
HTML::Pipeline.new [
9+
HTML::Pipeline::TableOfContentsFilter
10+
]
11+
12+
def toc
13+
result = {}
14+
TocPipeline.call(@orig, {}, result)
15+
result[:toc]
16+
end
17+
718
def test_anchors_are_added_properly
819
orig = %(<h1>Ice cube</h1><p>Will swarm on any motherfucker in a blue uniform</p>)
920
assert_includes '<a name=', TocFilter.call(orig).to_s
1021
end
1122

23+
def test_toc_list_added_properly
24+
@orig = %(<h1>Ice cube</h1><p>Will swarm on any motherfucker in a blue uniform</p>)
25+
assert_includes %Q{<ul class="section-nav">\n<li><a href="}, toc
26+
end
27+
1228
def test_anchors_have_sane_names
1329
orig = %(<h1>Dr Dre</h1><h1>Ice Cube</h1><h1>Eazy-E</h1><h1>MC Ren</h1>)
1430
result = TocFilter.call(orig).to_s
@@ -19,6 +35,14 @@ def test_anchors_have_sane_names
1935
assert_includes '"mc-ren"', result
2036
end
2137

38+
def test_toc_hrefs_have_sane_values
39+
@orig = %(<h1>Dr Dre</h1><h1>Ice Cube</h1><h1>Eazy-E</h1><h1>MC Ren</h1>)
40+
assert_includes '"#dr-dre"', toc
41+
assert_includes '"#ice-cube"', toc
42+
assert_includes '"#eazy-e"', toc
43+
assert_includes '"#mc-ren"', toc
44+
end
45+
2246
def test_dupe_headers_have_unique_trailing_identifiers
2347
orig = %(<h1>Straight Outta Compton</h1>
2448
<h2>Dopeman</h2>
@@ -31,6 +55,16 @@ def test_dupe_headers_have_unique_trailing_identifiers
3155
assert_includes '"dopeman-1"', result
3256
end
3357

58+
def test_dupe_headers_have_unique_toc_anchors
59+
@orig = %(<h1>Straight Outta Compton</h1>
60+
<h2>Dopeman</h2>
61+
<h3>Express Yourself</h3>
62+
<h1>Dopeman</h1>)
63+
64+
assert_includes '"#dopeman"', toc
65+
assert_includes '"#dopeman-1"', toc
66+
end
67+
3468
def test_all_header_tags_are_found_when_adding_anchors
3569
orig = %(<h1>"Funky President" by James Brown</h1>
3670
<h2>"It's My Thing" by Marva Whitney</h2>
@@ -41,18 +75,47 @@ def test_all_header_tags_are_found_when_adding_anchors
4175
<h7>"Be Thankful for What You Got" by William DeVaughn</h7>)
4276

4377
doc = TocFilter.call(orig)
78+
4479
assert_equal 6, doc.search('a').size
4580
end
4681

47-
def test_anchors_with_utf8_characters
48-
orig = %(<h1>日本語</h1>
49-
<h1>Русский</h1)
82+
def test_toc_is_complete
83+
@orig = %(<h1>"Funky President" by James Brown</h1>
84+
<h2>"It's My Thing" by Marva Whitney</h2>
85+
<h3>"Boogie Back" by Roy Ayers</h3>
86+
<h4>"Feel Good" by Fancy</h4>
87+
<h5>"Funky Drummer" by James Brown</h5>
88+
<h6>"Ruthless Villain" by Eazy-E</h6>
89+
<h7>"Be Thankful for What You Got" by William DeVaughn</h7>)
5090

51-
rendered_h1s = TocFilter.call(orig).search('h1').map(&:to_s)
91+
expected = %Q{<ul class="section-nav">\n<li><a href="#funky-president-by-james-brown">"Funky President" by James Brown</a></li>\n<li><a href="#its-my-thing-by-marva-whitney">"It's My Thing" by Marva Whitney</a></li>\n<li><a href="#boogie-back-by-roy-ayers">"Boogie Back" by Roy Ayers</a></li>\n<li><a href="#feel-good-by-fancy">"Feel Good" by Fancy</a></li>\n<li><a href="#funky-drummer-by-james-brown">"Funky Drummer" by James Brown</a></li>\n<li><a href="#ruthless-villain-by-eazy-e">"Ruthless Villain" by Eazy-E</a></li>\n</ul>}
5292

53-
assert_equal "<h1>\n<a name=\"%E6%97%A5%E6%9C%AC%E8%AA%9E\" class=\"anchor\" href=\"#%E6%97%A5%E6%9C%AC%E8%AA%9E\"><span class=\"octicon octicon-link\"></span></a>日本語</h1>",
54-
rendered_h1s[0]
55-
assert_equal "<h1>\n<a name=\"%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9\" class=\"anchor\" href=\"#%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9\"><span class=\"octicon octicon-link\"></span></a>Русский</h1>",
56-
rendered_h1s[1]
57-
end if RUBY_VERSION > "1.9" # not sure how to make this work on 1.8.7
93+
assert_equal expected, toc
94+
end
95+
96+
if RUBY_VERSION > "1.9" # not sure how to make this work on 1.8.7
97+
98+
def test_anchors_with_utf8_characters
99+
orig = %(<h1>日本語</h1>
100+
<h1>Русский</h1)
101+
102+
rendered_h1s = TocFilter.call(orig).search('h1').map(&:to_s)
103+
104+
assert_equal "<h1>\n<a name=\"%E6%97%A5%E6%9C%AC%E8%AA%9E\" class=\"anchor\" href=\"#%E6%97%A5%E6%9C%AC%E8%AA%9E\"><span class=\"octicon octicon-link\"></span></a>日本語</h1>",
105+
rendered_h1s[0]
106+
assert_equal "<h1>\n<a name=\"%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9\" class=\"anchor\" href=\"#%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9\"><span class=\"octicon octicon-link\"></span></a>Русский</h1>",
107+
rendered_h1s[1]
108+
end
109+
110+
def test_toc_with_utf8_characters
111+
@orig = %(<h1>日本語</h1>
112+
<h1>Русский</h1)
113+
114+
rendered_toc = Nokogiri::HTML::DocumentFragment.parse(toc).to_s
115+
116+
expected = %Q{<ul class="section-nav">\n<li><a href="#%E6%97%A5%E6%9C%AC%E8%AA%9E">日本語</a></li>\n<li><a href="#%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9">Русский</a></li>\n</ul>}
117+
118+
assert_equal expected, rendered_toc
119+
end
120+
end
58121
end

0 commit comments

Comments
 (0)