diff --git a/doc/user/markdown.md b/doc/user/markdown.md index fc2780074632383c4bfcf8e0484d149be0bf40aa..b7c2ea75ba626cb77c10189932d7e63c76b23705 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -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. 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: You can add a table of contents to issues and merge requests, but you can't add one diff --git a/lib/banzai/filter/table_of_contents_tag_filter.rb b/lib/banzai/filter/table_of_contents_tag_filter.rb index 13d0a6a4cc7b2fe435fdce9056f641af142e9cc7..4e80b543e2d08272185d2fe58ea83ddd75a55940 100644 --- a/lib/banzai/filter/table_of_contents_tag_filter.rb +++ b/lib/banzai/filter/table_of_contents_tag_filter.rb @@ -2,26 +2,31 @@ 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. + # Using `[[_TOC_]]` or `[TOC]` (both case insensitive), inserts a Table of Contents list. # + # `[[_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 # + # `[toc]` is a generally accepted form, used by Typora for example. + # # Based on Banzai::Filter::GollumTagsFilter 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 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) + if toc_tag?(node) + # Support [TOC] / [toc] tags, which don't have a wrapping <em>-tag + process_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 doc @@ -31,14 +36,25 @@ def call # Replace an entire `[[<em>TOC</em>]]` node with the result generated by # 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) - 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 - def toc_tag?(node) - node.content == 'TOC' && + def toc_tag_em?(node) + node.content.casecmp?('toc') && 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 diff --git a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb index 56f36af5066c55f3e0963ff229e55178bcef853b..082e5c92e5314592ec866fed0991ae939c1c9293 100644 --- a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb @@ -6,18 +6,42 @@ include FilterSpecHelper 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 - doc = filter(html, {}, { toc: "FOO" }) + expect(doc.to_html).to eq("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 - it 'handles an empty ToC result' do - doc = filter(html) + context '[[_toc_]] as tag' do + 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 diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index 989e06a992df89ed3975634a6c6457079491b2a8..72661003361eaf4355277374c6c5c188fd92e61d 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -102,33 +102,45 @@ describe 'table of contents' do let(:project) { create(:project, :public) } - let(:markdown) do - <<-MARKDOWN.strip_heredoc - [[_TOC_]] + + shared_examples 'table of contents tag' do |tag, tag_html| + let(:markdown) do + <<-MARKDOWN.strip_heredoc + #{tag} # Header - MARKDOWN - end + MARKDOWN + end - let(:invalid_markdown) do - <<-MARKDOWN.strip_heredoc - test [[_TOC_]] + let(:invalid_markdown) do + <<-MARKDOWN.strip_heredoc + test #{tag} # Header - MARKDOWN - end + MARKDOWN + end - it 'inserts a table of contents' do - output = described_class.to_html(markdown, project: project) + 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>") + 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 #{tag_html}") + end end - it 'does not insert a table of contents' do - output = described_class.to_html(invalid_markdown, project: project) + context 'with [[_TOC_]] as tag' do + 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 diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 007d310247bce33bdb5fc8cd68f4f43c6303298f..59f5e4a69001d77eb510f75df3ed9ed466e3c06f 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -27,7 +27,7 @@ end end - it 'is case-sensitive' do + it 'is not case-sensitive' do markdown = <<-MD.strip_heredoc [[_toc_]] @@ -36,9 +36,22 @@ Foo 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 it 'handles an empty pipeline result' do