Commit 2a055c23 authored by Akihiro Nakashima's avatar Akihiro Nakashima Committed by Robert Speicher

Fix indentation level in Wiki's TOC items to regard header level

parent 10fd3542
---
title: Wiki table of contents are now properly nested to reflect header level
merge_request: 13650
author: Akihiro Nakashima
type: fixed
...@@ -15,6 +15,7 @@ module Banzai ...@@ -15,6 +15,7 @@ module Banzai
# `li` child elements. # `li` child elements.
class TableOfContentsFilter < HTML::Pipeline::Filter class TableOfContentsFilter < HTML::Pipeline::Filter
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u
HeaderNode = Struct.new(:level, :href, :text, :children, :parent)
def call def call
return doc if context[:no_header_anchors] return doc if context[:no_header_anchors]
...@@ -23,6 +24,10 @@ module Banzai ...@@ -23,6 +24,10 @@ module Banzai
headers = Hash.new(0) headers = Hash.new(0)
# root node of header-tree
header_root = HeaderNode.new(0, nil, nil, [], nil)
current_header = header_root
doc.css('h1, h2, h3, h4, h5, h6').each do |node| doc.css('h1, h2, h3, h4, h5, h6').each do |node|
text = node.text text = node.text
...@@ -38,12 +43,38 @@ module Banzai ...@@ -38,12 +43,38 @@ module Banzai
# namespace detection will be automatically handled via javascript (see issue #22781) # namespace detection will be automatically handled via javascript (see issue #22781)
namespace = "user-content-" namespace = "user-content-"
href = "#{id}#{uniq}" href = "#{id}#{uniq}"
push_toc(href, text)
level = node.name[1].to_i # get this header level
if level == current_header.level
# same as previous
parent = current_header.parent
elsif level > current_header.level
# larger (weaker) than previous
parent = current_header
else
# smaller (stronger) than previous
# search parent
parent = current_header
parent = parent.parent while parent.level >= level
end
# create header-node and push as child
header_node = HeaderNode.new(level, href, text, [], parent)
parent.children.push(header_node)
current_header = header_node
header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href))
end end
end end
result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty? # extract header-tree
if header_root.children.length > 0
result[:toc] = %Q{<ul class="section-nav">\n}
header_root.children.each do |child|
push_toc(child)
end
result[:toc] << '</ul>'
end
doc doc
end end
...@@ -54,8 +85,16 @@ module Banzai ...@@ -54,8 +85,16 @@ module Banzai
%Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>} %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>}
end end
def push_toc(href, text) def push_toc(header_node)
result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n} result[:toc] << %Q{<li><a href="##{header_node.href}">#{header_node.text}</a>}
if header_node.children.length > 0
result[:toc] << '<ul>'
header_node.children.each do |child|
push_toc(child)
end
result[:toc] << '</ul>'
end
result[:toc] << '</li>\n'
end end
end end
end end
......
...@@ -78,7 +78,7 @@ describe Banzai::Filter::TableOfContentsFilter do ...@@ -78,7 +78,7 @@ describe Banzai::Filter::TableOfContentsFilter do
HTML::Pipeline.new([described_class]).call(html) HTML::Pipeline.new([described_class]).call(html)
end end
let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) } let(:results) { result(header(1, 'Header 1') + header(2, 'Header 1-1') + header(3, 'Header 1-1-1') + header(2, 'Header 1-2') + header(1, 'Header 2') + header(2, 'Header 2-1')) }
let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) } let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
it 'is contained within a `ul` element' do it 'is contained within a `ul` element' do
...@@ -87,14 +87,46 @@ describe Banzai::Filter::TableOfContentsFilter do ...@@ -87,14 +87,46 @@ describe Banzai::Filter::TableOfContentsFilter do
end end
it 'contains an `li` element for each header' do it 'contains an `li` element for each header' do
expect(doc.css('li').length).to eq 2 expect(doc.css('li').length).to eq 6
links = doc.css('li a') links = doc.css('li a')
expect(links.first.attr('href')).to eq '#header-1' expect(links[0].attr('href')).to eq '#header-1'
expect(links.first.text).to eq 'Header 1' expect(links[0].text).to eq 'Header 1'
expect(links.last.attr('href')).to eq '#header-2' expect(links[1].attr('href')).to eq '#header-1-1'
expect(links.last.text).to eq 'Header 2' expect(links[1].text).to eq 'Header 1-1'
expect(links[2].attr('href')).to eq '#header-1-1-1'
expect(links[2].text).to eq 'Header 1-1-1'
expect(links[3].attr('href')).to eq '#header-1-2'
expect(links[3].text).to eq 'Header 1-2'
expect(links[4].attr('href')).to eq '#header-2'
expect(links[4].text).to eq 'Header 2'
expect(links[5].attr('href')).to eq '#header-2-1'
expect(links[5].text).to eq 'Header 2-1'
end
it 'keeps list levels regarding header levels' do
items = doc.css('li')
# Header 1
expect(items[0].ancestors.any? {|node| node.name == 'li'}).to eq false
# Header 1-1
expect(items[1].ancestors.include?(items[0])).to eq true
# Header 1-1-1
expect(items[2].ancestors.include?(items[0])).to eq true
expect(items[2].ancestors.include?(items[1])).to eq true
# Header 1-2
expect(items[3].ancestors.include?(items[0])).to eq true
expect(items[3].ancestors.include?(items[1])).to eq false
# Header 2
expect(items[4].ancestors.any? {|node| node.name == 'li'}).to eq false
# Header 2-1
expect(items[5].ancestors.include?(items[4])).to eq true
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment