Commit 82894087 authored by Guillaume Grossetie's avatar Guillaume Grossetie Committed by Mayra Cabrera

Prefix inline anchor ids with `user-content-`

parent bb9ef246
---
title: Preserve cross references in AsciiDoc documents
merge_request: 47131
author: Guillaume Grossetie
type: changed
......@@ -6,8 +6,8 @@ module Banzai
#
# Extends Banzai::Filter::BaseSanitizationFilter with specific rules.
class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter
# Section anchor link pattern
SECTION_LINK_REF_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze
# Anchor link prefixed by "user-content-" pattern
PREFIXED_ID_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze
SECTION_HEADINGS = %w(h2 h3 h4 h5 h6).freeze
# Footnote link patterns
......@@ -54,44 +54,35 @@ module Banzai
whitelist[:attributes]['table'] = %w(class)
whitelist[:transformers].push(self.class.remove_element_classes)
# Allow `id` in anchor and footnote elements
whitelist[:attributes]['a'].push('id')
whitelist[:attributes]['div'].push('id')
# Allow `id` in heading elements for section anchors
SECTION_HEADINGS.each do |header|
whitelist[:attributes][header] = %w(id)
end
whitelist[:transformers].push(self.class.remove_non_heading_ids)
# Allow `id` in footnote elements
FOOTNOTE_LINK_ID_PATTERNS.keys.each do |element|
whitelist[:attributes][element.to_s].push('id')
end
whitelist[:transformers].push(self.class.remove_non_footnote_ids)
# Remove ids that are not explicitly allowed
whitelist[:transformers].push(self.class.remove_disallowed_ids)
whitelist
end
class << self
def remove_non_footnote_ids
def remove_disallowed_ids
lambda do |env|
node = env[:node]
return unless (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym])
return unless node.name == 'a' || node.name == 'div' || SECTION_HEADINGS.any?(node.name)
return unless node.has_attribute?('id')
return if node['id'] =~ pattern
return if node['id'] =~ PREFIXED_ID_PATTERN
node.remove_attribute('id')
end
if (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym])
return if node['id'] =~ pattern
end
def remove_non_heading_ids
lambda do |env|
node = env[:node]
return unless SECTION_HEADINGS.any?(node.name)
return unless node.has_attribute?('id')
return if node['id'] =~ SECTION_LINK_REF_PATTERN
node.remove_attribute('id')
end
end
......
......@@ -19,6 +19,12 @@ module Gitlab
%(<code#{id_attribute(node)} data-math-style="inline">#{node.text}</code>)
end
def convert_inline_anchor(node)
node.id = "user-content-#{node.id}" if node.id && !node.id.start_with?('user-content-')
super(node)
end
private
def id_attribute(node)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::AsciiDocSanitizationFilter do
include FilterSpecHelper
it 'preserves footnotes refs' do
result = filter('<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>').to_html
expect(result).to eq('<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>')
end
it 'preserves footnotes defs' do
result = filter('<div id="_footnotedef_1">
<a href="#_footnoteref_1">1</a>. This is the text of the footnote.</div>').to_html
expect(result).to eq(%(<div id="_footnotedef_1">
<a href="#_footnoteref_1">1</a>. This is the text of the footnote.</div>))
end
it 'preserves user-content- prefixed ids on anchors' do
result = filter('<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document.</p>').to_html
expect(result).to eq(%(<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document.</p>))
end
it 'preserves user-content- prefixed ids on div (blocks)' do
html_content = <<~HTML
<div id="user-content-open-block" class="openblock">
<div class="content">
<div class="paragraph">
<p>This is an open block</p>
</div>
</div>
</div>
HTML
output = <<~SANITIZED_HTML
<div id="user-content-open-block">
<div>
<div>
<p>This is an open block</p>
</div>
</div>
</div>
SANITIZED_HTML
expect(filter(html_content).to_html).to eq(output)
end
it 'preserves section anchor ids' do
result = filter(%(<h2 id="user-content-first-section">
<a class="anchor" href="#user-content-first-section"></a>First section</h2>)).to_html
expect(result).to eq(%(<h2 id="user-content-first-section">
<a class="anchor" href="#user-content-first-section"></a>First section</h2>))
end
it 'removes non prefixed ids' do
result = filter('<p><a id="cross-references"></a>A link to another location within an AsciiDoc document.</p>').to_html
expect(result).to eq(%(<p><a></a>A link to another location within an AsciiDoc document.</p>))
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Asciidoc::Html5Converter do
describe 'convert AsciiDoc to HTML5' do
it 'appends user-content- prefix on ref (anchor)' do
doc = Asciidoctor::Document.new('')
anchor = Asciidoctor::Inline.new(doc, :anchor, '', type: :ref, id: 'cross-references')
converter = Gitlab::Asciidoc::Html5Converter.new('gitlab_html5')
html = converter.convert_inline_anchor(anchor)
expect(html).to eq('<a id="user-content-cross-references"></a>')
end
end
end
......@@ -252,6 +252,27 @@ module Gitlab
end
end
context 'with xrefs' do
it 'preserves ids' do
input = <<~ADOC
Learn how to xref:cross-references[use cross references].
[[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
ADOC
output = <<~HTML
<div>
<p>Learn how to <a href="#cross-references">use cross references</a>.</p>
</div>
<div>
<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
</div>
HTML
expect(render(input, context)).to include(output.strip)
end
end
context 'with checklist' do
it 'preserves classes' do
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