Commit bdcd884f authored by Vitali Tatarintev's avatar Vitali Tatarintev

Add a new context to ImageLinkFilter

Allows to replace images with a link to an image
parent a9fb9fdc
# frozen_string_literal: true
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js
module Banzai
module Filter
# HTML filter that wraps links around inline images and replaces image with a link.
class ImageAttachmentLinkFilter < HTML::Pipeline::Filter
# Find every image that isn't already wrapped in an `a` tag, create
# a new node (a link to the image source), copy the image alternative text as a child
# of the anchor, and then replace the img with the link-wrapped version.
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
link = doc.document.create_element(
'a',
class: 'with-attachment-icon',
href: img['data-src'] || img['src'],
target: '_blank',
rel: 'noopener noreferrer'
)
# make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
link.children = img['alt'] || img['data-src'] || img['src']
img.replace(link)
end
doc
end
end
end
end
...@@ -12,7 +12,7 @@ module Banzai ...@@ -12,7 +12,7 @@ module Banzai
Filter::MarkdownFilter, Filter::MarkdownFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
Filter::ExternalLinkFilter, Filter::ExternalLinkFilter,
Filter::ImageAttachmentLinkFilter, Filter::ImageLinkFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
*reference_filters *reference_filters
] ]
...@@ -36,7 +36,8 @@ module Banzai ...@@ -36,7 +36,8 @@ module Banzai
Filter::AssetProxyFilter.transform_context(context).merge( Filter::AssetProxyFilter.transform_context(context).merge(
only_path: true, only_path: true,
no_sourcepos: true, no_sourcepos: true,
allowlist: ALLOWLIST allowlist: ALLOWLIST,
link_replaces_image: true
) )
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::ImageAttachmentLinkFilter do
include FilterSpecHelper
let(:path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
def image(path, alt: nil)
alt_tag = alt ? %Q{alt="#{alt}"} : ""
%(<img src="#{path}" #{alt_tag} />)
end
it 'replaces the image with link to image src', :aggregate_failures do
doc = filter(image(path))
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
it 'uses image alt as a link text', :aggregate_failures do
doc = filter(image(path, alt: 'My image'))
expect(doc.to_html).to match(%r{^<a[^>]*>My image</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
it 'adds attachment icon class to the link' do
doc = filter(image(path))
expect(doc.at_css('a')['class']).to match(%r{with-attachment-icon})
end
it 'does not wrap a duplicate link' do
doc = filter(%Q(<a href="/whatever">#{image(path)}</a>))
expect(doc.to_html).to match(%r{^<a href="/whatever"><img[^>]*></a>$})
end
it 'works with external images' do
external_path = 'https://i.imgur.com/DfssX9C.jpg'
doc = filter(image(external_path))
expect(doc.at_css('a')['href']).to eq(external_path)
end
it 'works with inline images' do
doc = filter(%Q(<p>test #{image(path)} inline</p>))
expect(doc.to_html).to match(%r{^<p>test <a[^>]*>#{path}</a> inline</p>$})
end
it 'keep the data-canonical-src' do
data_canonical_src = "http://example.com/test.png"
doc = filter(%Q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="#{data_canonical_src}" />))
expect(doc.at_css('a')['data-canonical-src']).to eq(data_canonical_src)
end
end
...@@ -8,11 +8,17 @@ module Banzai ...@@ -8,11 +8,17 @@ module Banzai
# Find every image that isn't already wrapped in an `a` tag, create # Find every image that isn't already wrapped in an `a` tag, create
# a new node (a link to the image source), copy the image as a child # a new node (a link to the image source), copy the image as a child
# of the anchor, and then replace the img with the link-wrapped version. # of the anchor, and then replace the img with the link-wrapped version.
#
# If `link_replaces_image` context parameter provided, the image is going
# to be replaced with a link to an image.
def call def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
link_replaces_image = !!context[:link_replaces_image]
html_class = link_replaces_image ? 'with-attachment-icon' : 'no-attachment-icon'
link = doc.document.create_element( link = doc.document.create_element(
'a', 'a',
class: 'no-attachment-icon', class: html_class,
href: img['data-src'] || img['src'], href: img['data-src'] || img['src'],
target: '_blank', target: '_blank',
rel: 'noopener noreferrer' rel: 'noopener noreferrer'
...@@ -21,7 +27,11 @@ module Banzai ...@@ -21,7 +27,11 @@ module Banzai
# make sure the original non-proxied src carries over to the link # make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src'] link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
link.children = img.clone link.children = if link_replaces_image
img['alt'] || img['data-src'] || img['src']
else
img.clone
end
img.replace(link) img.replace(link)
end end
......
...@@ -5,34 +5,62 @@ require 'spec_helper' ...@@ -5,34 +5,62 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::ImageLinkFilter do RSpec.describe Banzai::Filter::ImageLinkFilter do
include FilterSpecHelper include FilterSpecHelper
def image(path) let(:path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
%(<img src="#{path}" />) let(:context) { {} }
def image(path, alt: nil)
alt_tag = alt ? %Q{alt="#{alt}"} : ""
%(<img src="#{path}" #{alt_tag} />)
end end
it 'wraps the image with a link to the image src' do it 'wraps the image with a link to the image src' do
doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) doc = filter(image(path), context)
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
end end
it 'does not wrap a duplicate link' do it 'does not wrap a duplicate link' do
doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)) doc = filter(%Q(<a href="/whatever">#{image(path)}</a>), context)
expect(doc.to_html).to match %r{^<a href="/whatever"><img[^>]*></a>$} expect(doc.to_html).to match %r{^<a href="/whatever"><img[^>]*></a>$}
end end
it 'works with external images' do it 'works with external images' do
doc = filter(image('https://i.imgur.com/DfssX9C.jpg')) doc = filter(image('https://i.imgur.com/DfssX9C.jpg'), context)
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
end end
it 'works with inline images' do it 'works with inline images' do
doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>)) doc = filter(%Q(<p>test #{image(path)} inline</p>), context)
expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$} expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$}
end end
it 'keep the data-canonical-src' do it 'keep the data-canonical-src' do
doc = filter(%q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="http://example.com/test.png" />)) doc = filter(%q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="http://example.com/test.png" />), context)
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
expect(doc.at_css('img')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src'] expect(doc.at_css('img')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src']
end end
context 'when :link_replaces_image is true' do
let(:context) { { link_replaces_image: true } }
it 'replaces the image with link to image src', :aggregate_failures do
doc = filter(image(path), context)
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
it 'uses image alt as a link text', :aggregate_failures do
doc = filter(image(path, alt: 'My image'), context)
expect(doc.to_html).to match(%r{^<a[^>]*>My image</a>$})
expect(doc.at_css('a')['href']).to eq(path)
end
it 'adds attachment icon class to the link' do
doc = filter(image(path), context)
expect(doc.at_css('a')['class']).to match(%r{with-attachment-icon})
end
end
end end
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