Commit 84189487 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '21901-support-for-toc-in-readme-files' into 'master'

Support for table of contents (TOC) in markdown files and issue / MR descriptions

Closes #21901

See merge request gitlab-org/gitlab!24196
parents 007c3c55 9f5d4574
---
title: Support for table of contents tag in GitLab Flavored Markdown
merge_request: 24196
author:
type: added
......@@ -25,12 +25,10 @@ module Banzai
# * [[http://example.com/images/logo.png]]
# * [[http://example.com/images/logo.png|alt=Logo]]
#
# - Insert a Table of Contents list:
#
# * [[_TOC_]]
#
# Based on Gollum::Filter::Tags
#
# Note: the table of contents tag is now handled by TableOfContentsTagFilter
#
# Context options:
# :project_wiki (required) - Current project wiki.
#
......@@ -64,23 +62,11 @@ module Banzai
def call
doc.search(".//text()").each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
if toc_tag?(node)
process_toc_tag(node)
else
content = node.content
next unless content =~ TAGS_PATTERN
next unless node.content =~ TAGS_PATTERN
html = process_tag($1)
if html && html != node.content
node.replace(html)
end
end
node.replace(html) if html && html != node.content
end
doc
......@@ -88,12 +74,6 @@ module Banzai
private
# Replace an entire `[[<em>TOC</em>]]` node with the result generated by
# TableOfContentsFilter
def process_toc_tag(node)
node.parent.parent.replace(result[:toc].presence || '')
end
# Process a single tag into its final HTML form.
#
# tag - The String tag contents (the stuff inside the double brackets).
......@@ -129,12 +109,6 @@ module Banzai
end
end
def toc_tag?(node)
node.content == 'TOC' &&
node.parent.name == 'em' &&
node.parent.parent.text == '[[TOC]]'
end
def image?(path)
path =~ ALLOWED_IMAGE_EXTENSIONS
end
......
# frozen_string_literal: true
module Banzai
module Filter
# Using `[[_TOC_]]`, inserts a Table of Contents list.
# This syntax is based on the Gollum syntax. This way we have
# some consistency between with wiki and normal markdown.
# If there ever emerges a markdown standard, we can implement
# that here.
#
# The support for this has been removed from GollumTagsFilter
#
# Based on Banzai::Filter::GollumTagsFilter
class TableOfContentsTagFilter < HTML::Pipeline::Filter
TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(., 'TOC')])
def call
return doc if context[:no_header_anchors]
doc.xpath(TEXT_QUERY).each do |node|
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
process_toc_tag(node) if toc_tag?(node)
end
doc
end
private
# Replace an entire `[[<em>TOC</em>]]` node with the result generated by
# TableOfContentsFilter
def process_toc_tag(node)
node.parent.parent.replace(result[:toc].presence || '')
end
def toc_tag?(node)
node.content == 'TOC' &&
node.parent.name == 'em' &&
node.parent.parent.text == '[[TOC]]'
end
end
end
end
......@@ -32,6 +32,7 @@ module Banzai
Filter::InlineMetricsFilter,
Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
Filter::SuggestionFilter,
......
......@@ -100,19 +100,4 @@ describe Banzai::Filter::GollumTagsFilter do
expect(doc.at_css('code').text).to eq '[[link-in-backticks]]'
end
end
context 'table of contents' do
it 'replaces [[<em>TOC</em>]] with ToC result' do
doc = described_class.call("<p>[[<em>TOC</em>]]</p>", { project_wiki: project_wiki }, { toc: "FOO" })
expect(doc.to_html).to eq("FOO")
end
it 'handles an empty ToC result' do
input = "<p>[[<em>TOC</em>]]</p>"
doc = described_class.call(input, project_wiki: project_wiki)
expect(doc.to_html).to eq ''
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::TableOfContentsTagFilter do
include FilterSpecHelper
context 'table of contents' do
let(:html) { '<p>[[<em>TOC</em>]]</p>' }
it 'replaces [[<em>TOC</em>]] with ToC result' do
doc = filter(html, {}, { toc: "FOO" })
expect(doc.to_html).to eq("FOO")
end
it 'handles an empty ToC result' do
doc = filter(html)
expect(doc.to_html).to eq ''
end
end
end
......@@ -99,4 +99,35 @@ describe Banzai::Pipeline::FullPipeline do
end
end
end
describe 'table of contents' do
let(:project) { create(:project, :public) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
[[_TOC_]]
# Header
MARKDOWN
end
let(:invalid_markdown) do
<<-MARKDOWN.strip_heredoc
test [[_TOC_]]
# Header
MARKDOWN
end
it 'inserts a table of contents' do
output = described_class.to_html(markdown, project: project)
expect(output).to include("<ul class=\"section-nav\">")
expect(output).to include("<li><a href=\"#header\">Header</a></li>")
end
it 'does not insert a table of contents' do
output = described_class.to_html(invalid_markdown, project: project)
expect(output).to include("test [[<em>TOC</em>]]")
end
end
end
......@@ -15,7 +15,7 @@ module FilterSpecHelper
# context - Hash context for the filter. (default: {project: project})
#
# Returns a Nokogiri::XML::DocumentFragment
def filter(html, context = {})
def filter(html, context = {}, result = nil)
if defined?(project)
context.reverse_merge!(project: project)
end
......@@ -25,7 +25,7 @@ module FilterSpecHelper
context = context.merge(render_context: render_context)
described_class.call(html, context)
described_class.call(html, context, result)
end
# Get an instance of the Filter class
......
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