Commit 0cba81c0 authored by Guillaume Grossetie's avatar Guillaume Grossetie

Enable section anchors

parent 1def0719
---
title: "Enable section anchors in Asciidoctor"
merge_request: 30666
author: Guillaume Grossetie
type: added
\ No newline at end of file
...@@ -6,6 +6,9 @@ module Banzai ...@@ -6,6 +6,9 @@ module Banzai
# #
# Extends Banzai::Filter::BaseSanitizationFilter with specific rules. # Extends Banzai::Filter::BaseSanitizationFilter with specific rules.
class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter
# Section anchor link pattern
SECTION_LINK_REF_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze
# Classes used by Asciidoctor to style components # Classes used by Asciidoctor to style components
ADMONITION_CLASSES = %w(fa icon-note icon-tip icon-warning icon-caution icon-important).freeze ADMONITION_CLASSES = %w(fa icon-note icon-tip icon-warning icon-caution icon-important).freeze
CALLOUT_CLASSES = ['conum'].freeze CALLOUT_CLASSES = ['conum'].freeze
...@@ -19,14 +22,17 @@ module Banzai ...@@ -19,14 +22,17 @@ module Banzai
td: ['icon'].freeze, td: ['icon'].freeze,
i: ADMONITION_CLASSES + CALLOUT_CLASSES + CHECKLIST_CLASSES, i: ADMONITION_CLASSES + CALLOUT_CLASSES + CHECKLIST_CLASSES,
ul: LIST_CLASSES, ul: LIST_CLASSES,
ol: LIST_CLASSES ol: LIST_CLASSES,
a: ['anchor'].freeze
}.freeze }.freeze
ALLOWED_HEADERS = %w(h2 h3 h4 h5 h6).freeze
def customize_whitelist(whitelist) def customize_whitelist(whitelist)
# Allow marks # Allow marks
whitelist[:elements].push('mark') whitelist[:elements].push('mark')
# Allow any classes in `span`, `i`, `div`, `td`, `ul` and `ol` elements # Allow any classes in `span`, `i`, `div`, `td`, `ul`, `ol` and `a` elements
# but then remove any unknown classes # but then remove any unknown classes
whitelist[:attributes]['span'] = %w(class) whitelist[:attributes]['span'] = %w(class)
whitelist[:attributes]['div'].push('class') whitelist[:attributes]['div'].push('class')
...@@ -34,12 +40,32 @@ module Banzai ...@@ -34,12 +40,32 @@ module Banzai
whitelist[:attributes]['i'] = %w(class) whitelist[:attributes]['i'] = %w(class)
whitelist[:attributes]['ul'] = %w(class) whitelist[:attributes]['ul'] = %w(class)
whitelist[:attributes]['ol'] = %w(class) whitelist[:attributes]['ol'] = %w(class)
whitelist[:attributes]['a'].push('class')
whitelist[:transformers].push(self.class.remove_element_classes) whitelist[:transformers].push(self.class.remove_element_classes)
# Allow `id` in heading elements for section anchors
ALLOWED_HEADERS.each do |header|
whitelist[:attributes][header] = %w(id)
end
whitelist[:transformers].push(self.class.remove_non_heading_ids)
whitelist whitelist
end end
class << self class << self
def remove_non_heading_ids
lambda do |env|
node = env[:node]
return unless ALLOWED_HEADERS.any?(node.name)
return unless node.has_attribute?('id')
return if node['id'] =~ SECTION_LINK_REF_PATTERN
node.remove_attribute('id')
end
end
def remove_element_classes def remove_element_classes
lambda do |env| lambda do |env|
node = env[:node] node = env[:node]
......
...@@ -13,6 +13,7 @@ module Gitlab ...@@ -13,6 +13,7 @@ module Gitlab
MAX_INCLUDE_DEPTH = 5 MAX_INCLUDE_DEPTH = 5
DEFAULT_ADOC_ATTRS = { DEFAULT_ADOC_ATTRS = {
'showtitle' => true, 'showtitle' => true,
'sectanchors' => true,
'idprefix' => 'user-content-', 'idprefix' => 'user-content-',
'idseparator' => '-', 'idseparator' => '-',
'env' => 'gitlab', 'env' => 'gitlab',
......
...@@ -96,6 +96,75 @@ module Gitlab ...@@ -96,6 +96,75 @@ module Gitlab
end end
end end
context 'with passthrough' do
it 'removes non heading ids' do
input = <<~ADOC
++++
<h2 id="foo">Title</h2>
++++
ADOC
output = <<~HTML
<h2>Title</h2>
HTML
expect(render(input, context)).to include(output.strip)
end
end
context 'with section anchors' do
it 'preserves ids and links' do
input = <<~ADOC
= Title
== First section
This is the first section.
== Second section
This is the second section.
== Thunder ⚡ !
This is the third section.
ADOC
output = <<~HTML
<h1>Title</h1>
<div>
<h2 id="user-content-first-section">
<a class="anchor" href="#user-content-first-section"></a>First section</h2>
<div>
<div>
<p>This is the first section.</p>
</div>
</div>
</div>
<div>
<h2 id="user-content-second-section">
<a class="anchor" href="#user-content-second-section"></a>Second section</h2>
<div>
<div>
<p>This is the second section.</p>
</div>
</div>
</div>
<div>
<h2 id="user-content-thunder">
<a class="anchor" href="#user-content-thunder"></a>Thunder ⚡ !</h2>
<div>
<div>
<p>This is the third section.</p>
</div>
</div>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end
context 'with checklist' do context 'with checklist' do
it 'preserves classes' do it 'preserves classes' do
input = <<~ADOC input = <<~ADOC
......
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