debian-mirror-gitlab/lib/banzai/filter/table_of_contents_filter.rb

123 lines
3.5 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2019-03-02 22:35:43 +05:30
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
2015-12-23 02:04:40 +05:30
module Banzai
module Filter
2015-09-11 14:41:01 +05:30
# HTML filter that adds an anchor child element to all Headers in a
# document, so that they can be linked to.
#
# Generates the Table of Contents with links to each header. See Results.
#
# Based on HTML::Pipeline::TableOfContentsFilter.
#
# Context options:
# :no_header_anchors - Skips all processing done by this filter.
#
# Results:
# :toc - String containing Table of Contents data as a `ul` element with
# `li` child elements.
class TableOfContentsFilter < HTML::Pipeline::Filter
2019-07-31 22:56:46 +05:30
PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze
2015-09-11 14:41:01 +05:30
def call
return doc if context[:no_header_anchors]
2018-11-18 11:00:15 +05:30
result[:toc] = +""
2015-09-11 14:41:01 +05:30
headers = Hash.new(0)
2018-03-17 18:26:18 +05:30
header_root = current_header = HeaderNode.new
2015-09-11 14:41:01 +05:30
doc.css('h1, h2, h3, h4, h5, h6').each do |node|
2018-03-17 18:26:18 +05:30
if header_content = node.children.first
id = node
.text
2019-07-31 22:56:46 +05:30
.strip
2018-03-17 18:26:18 +05:30
.downcase
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
.gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs
2015-09-11 14:41:01 +05:30
2018-03-17 18:26:18 +05:30
uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1
href = "#{id}#{uniq}"
2015-09-11 14:41:01 +05:30
2018-03-17 18:26:18 +05:30
current_header = HeaderNode.new(node: node, href: href, previous_header: current_header)
2015-09-11 14:41:01 +05:30
2018-03-17 18:26:18 +05:30
header_content.add_previous_sibling(anchor_tag(href))
2015-09-11 14:41:01 +05:30
end
end
2018-03-17 18:26:18 +05:30
push_toc(header_root.children, root: true)
2015-09-11 14:41:01 +05:30
doc
end
private
2018-03-17 18:26:18 +05:30
def anchor_tag(href)
2019-12-21 20:55:43 +05:30
escaped_href = CGI.escape(href) # account for non-ASCII characters
%Q{<a id="user-content-#{href}" class="anchor" href="##{escaped_href}" aria-hidden="true"></a>}
2015-09-11 14:41:01 +05:30
end
2018-03-17 18:26:18 +05:30
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
2019-12-21 20:55:43 +05:30
@href = CGI.escape(href) if href
2018-03-17 18:26:18 +05:30
@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
2018-06-27 16:04:02 +05:30
@text ||= EscapeUtils.escape_html(node.text)
2018-03-17 18:26:18 +05:30
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
parent
end
2015-09-11 14:41:01 +05:30
end
end
end
end