Commit cc6f7b0b authored by Sean McGivern's avatar Sean McGivern

Merge branch 'anakashima/gitlab-ce-fix_wiki_toc_indent' into 'master'

Wiki table of contents are now properly nested to reflect header level

See merge request !13909
parents 86cbf60c 056158ef
---
title: Wiki table of contents are now properly nested to reflect header level
merge_request: 13650
author: Akihiro Nakashima
type: fixed
...@@ -22,40 +22,94 @@ module Banzai ...@@ -22,40 +22,94 @@ module Banzai
result[:toc] = "" result[:toc] = ""
headers = Hash.new(0) headers = Hash.new(0)
header_root = current_header = HeaderNode.new
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 if header_content = node.children.first
id = node
id = text.downcase .text
id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation .downcase
id.tr!(' ', '-') # replace spaces with dash .gsub(PUNCTUATION_REGEXP, '') # remove punctuation
id.squeeze!('-') # replace multiple dashes with one .tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1 headers[id] += 1
if header_content = node.children.first
# namespace detection will be automatically handled via javascript (see issue #22781)
namespace = "user-content-"
href = "#{id}#{uniq}" href = "#{id}#{uniq}"
push_toc(href, text)
header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) current_header = HeaderNode.new(node: node, href: href, previous_header: current_header)
header_content.add_previous_sibling(anchor_tag(href))
end end
end end
result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty? push_toc(header_root.children, root: true)
doc doc
end end
private private
def anchor_tag(id, href) def anchor_tag(href)
%Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>} %Q{<a id="user-content-#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>}
end
def push_toc(children, root: false)
return if children.empty?
klass = ' class="section-nav"' if root
result[:toc] << "<ul#{klass}>"
children.each { |child| push_anchor(child) }
result[:toc] << '</ul>'
end
def push_anchor(header_node)
result[:toc] << %Q{<li><a href="##{header_node.href}">#{header_node.text}</a>}
push_toc(header_node.children)
result[:toc] << '</li>'
end
class HeaderNode
attr_reader :node, :href, :parent, :children
def initialize(node: nil, href: nil, previous_header: nil)
@node = node
@href = href
@children = []
@parent = find_parent(previous_header)
@parent.children.push(self) if @parent
end
def level
return 0 unless node
@level ||= node.name[1].to_i
end
def text
return '' unless node
@text ||= node.text
end
private
def find_parent(previous_header)
return unless previous_header
if level == previous_header.level
parent = previous_header.parent
elsif level > previous_header.level
parent = previous_header
else
parent = previous_header
parent = parent.parent while parent.level >= level
end end
def push_toc(href, text) parent
result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n} end
end end
end end
end end
......
...@@ -96,5 +96,41 @@ describe Banzai::Filter::TableOfContentsFilter do ...@@ -96,5 +96,41 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(links.last.attr('href')).to eq '#header-2' expect(links.last.attr('href')).to eq '#header-2'
expect(links.last.text).to eq 'Header 2' expect(links.last.text).to eq 'Header 2'
end end
context 'table of contents nesting' do
let(:results) do
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')
)
end
it 'keeps list levels regarding header levels' do
items = doc.css('li')
# Header 1
expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' }
# Header 1-1
expect(items[1].ancestors).to include(items[0])
# Header 1-1-1
expect(items[2].ancestors).to include(items[0], items[1])
# Header 1-2
expect(items[3].ancestors).to include(items[0])
expect(items[3].ancestors).not_to include(items[1])
# Header 2
expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' }
# Header 2-1
expect(items[5].ancestors).to include(items[4])
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