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
Filter::MarkdownFilter,
Filter::EmojiFilter,
Filter::ExternalLinkFilter,
Filter::ImageAttachmentLinkFilter,
Filter::ImageLinkFilter,
Filter::SanitizationFilter,
*reference_filters
]
......@@ -36,7 +36,8 @@ module Banzai
Filter::AssetProxyFilter.transform_context(context).merge(
only_path: true,
no_sourcepos: true,
allowlist: ALLOWLIST
allowlist: ALLOWLIST,
link_replaces_image: true
)
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
# 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
# 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
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(
'a',
class: 'no-attachment-icon',
class: html_class,
href: img['data-src'] || img['src'],
target: '_blank',
rel: 'noopener noreferrer'
......@@ -21,7 +27,11 @@ module Banzai
# 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.clone
link.children = if link_replaces_image
img['alt'] || img['data-src'] || img['src']
else
img.clone
end
img.replace(link)
end
......
......@@ -5,34 +5,62 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::ImageLinkFilter do
include FilterSpecHelper
def image(path)
%(<img src="#{path}" />)
let(:path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
let(:context) { {} }
def image(path, alt: nil)
alt_tag = alt ? %Q{alt="#{alt}"} : ""
%(<img src="#{path}" #{alt_tag} />)
end
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']
end
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>$}
end
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']
end
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>$}
end
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')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src']
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
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