diff --git a/changelogs/unreleased/64645-asciidoctor-preserve-footnote-link-ids.yml b/changelogs/unreleased/64645-asciidoctor-preserve-footnote-link-ids.yml new file mode 100644 index 00000000000..5427a035478 --- /dev/null +++ b/changelogs/unreleased/64645-asciidoctor-preserve-footnote-link-ids.yml @@ -0,0 +1,5 @@ +--- +title: "Preserve footnote link ids in Asciidoctor" +merge_request: 30790 +author: Guillaume Grossetie +type: added \ No newline at end of file diff --git a/lib/banzai/filter/ascii_doc_sanitization_filter.rb b/lib/banzai/filter/ascii_doc_sanitization_filter.rb index d8d63075752..9105e86ad04 100644 --- a/lib/banzai/filter/ascii_doc_sanitization_filter.rb +++ b/lib/banzai/filter/ascii_doc_sanitization_filter.rb @@ -8,12 +8,18 @@ module Banzai class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter # Section anchor link pattern SECTION_LINK_REF_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze + SECTION_HEADINGS = %w(h2 h3 h4 h5 h6).freeze + + # Footnote link patterns + FOOTNOTE_LINK_ID_PATTERNS = { + a: /\A_footnoteref_\d+\z/, + div: /\A_footnotedef_\d+\z/ + }.freeze # Classes used by Asciidoctor to style components ADMONITION_CLASSES = %w(fa icon-note icon-tip icon-warning icon-caution icon-important).freeze CALLOUT_CLASSES = ['conum'].freeze CHECKLIST_CLASSES = %w(fa fa-check-square-o fa-square-o).freeze - LIST_CLASSES = %w(checklist none no-bullet unnumbered unstyled).freeze ELEMENT_CLASSES_WHITELIST = { @@ -26,8 +32,6 @@ module Banzai a: ['anchor'].freeze }.freeze - ALLOWED_HEADERS = %w(h2 h3 h4 h5 h6).freeze - def customize_whitelist(whitelist) # Allow marks whitelist[:elements].push('mark') @@ -44,20 +48,39 @@ module Banzai whitelist[:transformers].push(self.class.remove_element_classes) # Allow `id` in heading elements for section anchors - ALLOWED_HEADERS.each do |header| + 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) + whitelist end class << self + def remove_non_footnote_ids + lambda do |env| + node = env[:node] + + return unless (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym]) + return unless node.has_attribute?('id') + + return if node['id'] =~ pattern + + node.remove_attribute('id') + end + end + def remove_non_heading_ids lambda do |env| node = env[:node] - return unless ALLOWED_HEADERS.any?(node.name) + return unless SECTION_HEADINGS.any?(node.name) return unless node.has_attribute?('id') return if node['id'] =~ SECTION_LINK_REF_PATTERN diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 5293732ead1..cbd4a509a55 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -110,6 +110,56 @@ module Gitlab expect(render(input, context)).to include(output.strip) end + + it 'removes non footnote def ids' do + input = <<~ADOC + ++++ +
Footnote definition
+ ++++ + ADOC + + output = <<~HTML +
Footnote definition
+ HTML + + expect(render(input, context)).to include(output.strip) + end + + it 'removes non footnote ref ids' do + input = <<~ADOC + ++++ + Footnote reference + ++++ + ADOC + + output = <<~HTML + Footnote reference + HTML + + expect(render(input, context)).to include(output.strip) + end + end + + context 'with footnotes' do + it 'preserves ids and links' do + input = <<~ADOC + This paragraph has a footnote.footnote:[This is the text of the footnote.] + ADOC + + output = <<~HTML +
+

This paragraph has a footnote.[1]

+
+
+
+
+ 1. This is the text of the footnote. +
+
+ HTML + + expect(render(input, context)).to include(output.strip) + end end context 'with section anchors' do