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

Merge branch '14193-support-toc-markdown' into 'master'

Add support for [TOC] as alias for [[_TOC_]] in Markdown

See merge request gitlab-org/gitlab!66329
parents a960285b fe7d45b1
...@@ -410,7 +410,7 @@ To create a task list, follow the format of an ordered or unordered list: ...@@ -410,7 +410,7 @@ To create a task list, follow the format of an ordered or unordered list:
A table of contents is an unordered list that links to subheadings in the document. A table of contents is an unordered list that links to subheadings in the document.
To add a table of contents to a Markdown file, wiki page, issue request, or merge request To add a table of contents to a Markdown file, wiki page, issue request, or merge request
description, add the `[[_TOC_]]` tag on its own line. description, add either the `[[_TOC_]]` or `[TOC]` tag on its own line.
NOTE: NOTE:
You can add a table of contents to issues and merge requests, but you can't add one You can add a table of contents to issues and merge requests, but you can't add one
......
...@@ -2,26 +2,31 @@ ...@@ -2,26 +2,31 @@
module Banzai module Banzai
module Filter module Filter
# Using `[[_TOC_]]`, inserts a Table of Contents list. # Using `[[_TOC_]]` or `[TOC]` (both case insensitive), 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.
# #
# `[[_TOC_]]` is based on the Gollum syntax. This way we have
# some consistency between with wiki and normal markdown.
# The support for this has been removed from GollumTagsFilter # The support for this has been removed from GollumTagsFilter
# #
# `[toc]` is a generally accepted form, used by Typora for example.
#
# Based on Banzai::Filter::GollumTagsFilter # Based on Banzai::Filter::GollumTagsFilter
class TableOfContentsTagFilter < HTML::Pipeline::Filter class TableOfContentsTagFilter < HTML::Pipeline::Filter
TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(., 'TOC')]) TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(translate(., 'TOC', 'toc'), 'toc')])
def call def call
return doc if context[:no_header_anchors] return doc if context[:no_header_anchors]
doc.xpath(TEXT_QUERY).each do |node| doc.xpath(TEXT_QUERY).each do |node|
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running if toc_tag?(node)
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it # Support [TOC] / [toc] tags, which don't have a wrapping <em>-tag
# needs special-case handling process_toc_tag(node)
process_toc_tag(node) if toc_tag?(node) elsif toc_tag_em?(node)
# Support Gollum like ToC tag (`[[_TOC_]]` / `[[_toc_]]`), which will be converted
# into `[[<em>TOC</em>]]` by the markdown filter, so it
# needs special-case handling
process_toc_tag_em(node)
end
end end
doc doc
...@@ -31,14 +36,25 @@ module Banzai ...@@ -31,14 +36,25 @@ module Banzai
# Replace an entire `[[<em>TOC</em>]]` node with the result generated by # Replace an entire `[[<em>TOC</em>]]` node with the result generated by
# TableOfContentsFilter # TableOfContentsFilter
def process_toc_tag_em(node)
process_toc_tag(node.parent)
end
# Replace an entire `[TOC]` node with the result generated by
# TableOfContentsFilter
def process_toc_tag(node) def process_toc_tag(node)
node.parent.parent.replace(result[:toc].presence || '') # we still need to go one step up to also replace the surrounding <p></p>
node.parent.replace(result[:toc].presence || '')
end end
def toc_tag?(node) def toc_tag_em?(node)
node.content == 'TOC' && node.content.casecmp?('toc') &&
node.parent.name == 'em' && node.parent.name == 'em' &&
node.parent.parent.text == '[[TOC]]' node.parent.parent.text.casecmp?('[[toc]]')
end
def toc_tag?(node)
node.parent.text.casecmp?('[toc]')
end end
end end
end end
......
...@@ -6,18 +6,42 @@ RSpec.describe Banzai::Filter::TableOfContentsTagFilter do ...@@ -6,18 +6,42 @@ RSpec.describe Banzai::Filter::TableOfContentsTagFilter do
include FilterSpecHelper include FilterSpecHelper
context 'table of contents' do context 'table of contents' do
let(:html) { '<p>[[<em>TOC</em>]]</p>' } shared_examples 'table of contents tag' do
it 'replaces toc tag with ToC result' do
doc = filter(html, {}, { toc: "FOO" })
it 'replaces [[<em>TOC</em>]] with ToC result' do expect(doc.to_html).to eq("FOO")
doc = filter(html, {}, { toc: "FOO" }) end
expect(doc.to_html).to eq("FOO") it 'handles an empty ToC result' do
doc = filter(html)
expect(doc.to_html).to eq ''
end
end
context '[[_TOC_]] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[[<em>TOC</em>]]</p>' }
end
end end
it 'handles an empty ToC result' do context '[[_toc_]] as tag' do
doc = filter(html) it_behaves_like 'table of contents tag' do
let(:html) { '<p>[[<em>toc</em>]]</p>' }
end
end
context '[TOC] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[TOC]</p>' }
end
end
expect(doc.to_html).to eq '' context '[toc] as tag' do
it_behaves_like 'table of contents tag' do
let(:html) { '<p>[toc]</p>' }
end
end end
end end
end end
...@@ -102,33 +102,45 @@ RSpec.describe Banzai::Pipeline::FullPipeline do ...@@ -102,33 +102,45 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
describe 'table of contents' do describe 'table of contents' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc shared_examples 'table of contents tag' do |tag, tag_html|
[[_TOC_]] let(:markdown) do
<<-MARKDOWN.strip_heredoc
#{tag}
# Header # Header
MARKDOWN MARKDOWN
end end
let(:invalid_markdown) do let(:invalid_markdown) do
<<-MARKDOWN.strip_heredoc <<-MARKDOWN.strip_heredoc
test [[_TOC_]] test #{tag}
# Header # Header
MARKDOWN MARKDOWN
end end
it 'inserts a table of contents' do it 'inserts a table of contents' do
output = described_class.to_html(markdown, project: project) output = described_class.to_html(markdown, project: project)
expect(output).to include("<ul class=\"section-nav\">") expect(output).to include("<ul class=\"section-nav\">")
expect(output).to include("<li><a href=\"#header\">Header</a></li>") 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 #{tag_html}")
end
end end
it 'does not insert a table of contents' do context 'with [[_TOC_]] as tag' do
output = described_class.to_html(invalid_markdown, project: project) it_behaves_like 'table of contents tag', '[[_TOC_]]', '[[<em>TOC</em>]]'
end
expect(output).to include("test [[<em>TOC</em>]]") context 'with [toc] as tag' do
it_behaves_like 'table of contents tag', '[toc]', '[toc]'
it_behaves_like 'table of contents tag', '[TOC]', '[TOC]'
end end
end end
......
...@@ -27,7 +27,7 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do ...@@ -27,7 +27,7 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do
end end
end end
it 'is case-sensitive' do it 'is not case-sensitive' do
markdown = <<-MD.strip_heredoc markdown = <<-MD.strip_heredoc
[[_toc_]] [[_toc_]]
...@@ -36,9 +36,22 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do ...@@ -36,9 +36,22 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do
Foo Foo
MD MD
output = described_class.to_html(markdown, project: project, wiki: wiki) result = described_class.call(markdown, project: project, wiki: wiki)
expect(result[:output].to_html).to include(result[:toc])
end
it 'works with alternative [toc] tag' do
markdown = <<-MD.strip_heredoc
[toc]
expect(output).to include('[[<em>toc</em>]]') # Header 1
Foo
MD
result = described_class.call(markdown, project: project, wiki: wiki)
expect(result[:output].to_html).to include(result[:toc])
end end
it 'handles an empty pipeline result' do it 'handles an empty pipeline result' do
......
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