Skip to content

Commit

Permalink
Merge pull request #351 from gjtorikian/multiple-dependencies
Browse files Browse the repository at this point in the history
Support multiple dependencies per filter
  • Loading branch information
gjtorikian authored Jan 8, 2021
2 parents f8643c0 + 79b0e21 commit 4745c88
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 34 deletions.
14 changes: 6 additions & 8 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in html-pipeline.gemspec
gemspec

group :development, :test do
gem 'awesome_print'
gem 'awesome_print'

gem 'rubocop'
gem 'rubocop-performance'
gem 'rubocop-standard'
end
gem 'rubocop'
gem 'rubocop-performance'
gem 'rubocop-standard'

group :development do
gem 'bundler'
Expand All @@ -20,8 +18,8 @@ end

group :test do
gem 'commonmarker', '~> 0.16', require: false
gem 'email_reply_parser', '~> 0.5', require: false
gem 'gemoji', '~> 2.0', require: false
gem 'gemoji', '~> 3.0', require: false
gem 'gemojione', '~> 4.3', require: false
gem 'minitest'
gem 'rinku', '~> 1.7', require: false
gem 'sanitize', '~> 5.2', require: false
Expand Down
33 changes: 33 additions & 0 deletions lib/html/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,39 @@ def self.require_dependency(name, requirer)
"Missing dependency '#{name}' for #{requirer}. See README.md for details.\n#{e.class.name}: #{e}"
end

def self.require_dependencies(names, requirer)
dependency_list = names.dup
loaded = false

while !loaded && names.length > 1
name = names.shift

begin
require_dependency(name, requirer)
loaded = true # we got a dependency
define_dependency_loaded_method(name, true)
# try the next dependency
rescue MissingDependencyError
define_dependency_loaded_method(name, false)
end
end

return if loaded

begin
name = names.shift
require name
define_dependency_loaded_method(name, true)
rescue LoadError => e
raise MissingDependencyError,
"Missing all dependencies '#{dependency_list.join(', ')}' for #{requirer}. See README.md for details.\n#{e.class.name}: #{e}"
end
end

def self.define_dependency_loaded_method(name, value)
self.class.define_method :"#{name}_loaded?", -> { value }
end

# Our DOM implementation.
DocumentFragment = Nokogiri::HTML::DocumentFragment

Expand Down
25 changes: 15 additions & 10 deletions lib/html/pipeline/emoji_filter.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require 'cgi'
HTML::Pipeline.require_dependency('gemoji', 'EmojiFilter')
HTML::Pipeline.require_dependencies(%w[gemoji gemojione], 'EmojiFilter')

module HTML
class Pipeline
Expand Down Expand Up @@ -41,7 +41,7 @@ def validate
#
# Returns a String with :emoji: replaced with images.
def emoji_image_filter(text)
text.gsub(emoji_pattern) do |_match|
text.gsub(emoji_pattern) do
emoji_image_tag(Regexp.last_match(1))
end
end
Expand Down Expand Up @@ -82,12 +82,16 @@ def asset_path(name)
end

# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
def emoji_pattern
@emoji_pattern ||= /:(#{emoji_names.map { |name| Regexp.escape(name) }.join('|')}):/
end

def self.emoji_names
Emoji.all.map(&:aliases).flatten.sort
def emoji_names
if self.class.gemoji_loaded?
Emoji.all.map(&:aliases)
else
Gemojione::Index.new.all.map { |i| i[1]['name'] }
end.flatten.sort
end

# Default attributes for img tag
Expand All @@ -107,12 +111,13 @@ def self.emoji_names
File.join(asset_root, asset_path(name))
end

private def emoji_pattern
self.class.emoji_pattern
end

private def emoji_filename(name)
Emoji.find_by_alias(name).image_filename
if self.class.gemoji_loaded?
Emoji.find_by_alias(name).image_filename
else
# replace their asset_host with ours
Gemojione.image_url_for_name(name).sub(Gemojione.asset_host, '')
end
end

# Return ancestor tags to stop the emojification.
Expand Down
51 changes: 37 additions & 14 deletions test/html/pipeline/emoji_filter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,117 @@
require 'test_helper'

class HTML::Pipeline::EmojiFilterTest < Minitest::Test
EmojiFilter = HTML::Pipeline::EmojiFilter
def setup
@emoji_filter = HTML::Pipeline::EmojiFilter
end

def test_emojify
filter = EmojiFilter.new('<p>:shipit:</p>', context: { asset_root: 'https://foo.com' })
filter = @emoji_filter.new('<p>:shipit:</p>', context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_match 'https://foo.com/emoji/shipit.png', doc.search('img').attr('src').value
end

def test_uri_encoding
filter = EmojiFilter.new('<p>:+1:</p>', context: { asset_root: 'https://foo.com' })
filter = @emoji_filter.new('<p>:+1:</p>', context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_match 'https://foo.com/emoji/unicode/1f44d.png', doc.search('img').attr('src').value
end

def test_required_context_validation
exception = assert_raises(ArgumentError) do
EmojiFilter.call('', context: {})
@emoji_filter.call('', context: {})
end
assert_match(/:asset_root/, exception.message)
end

def test_custom_asset_path
filter = EmojiFilter.new('<p>:+1:</p>', context: { asset_path: ':file_name', asset_root: 'https://foo.com' })
filter = @emoji_filter.new('<p>:+1:</p>', context: { asset_path: ':file_name', asset_root: 'https://foo.com' })
doc = filter.call
assert_match 'https://foo.com/unicode/1f44d.png', doc.search('img').attr('src').value
end

def test_not_emojify_in_code_tags
body = '<code>:shipit:</code>'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com' })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_equal body, doc.to_html
end

def test_not_emojify_in_tt_tags
body = '<tt>:shipit:</tt>'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com' })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_equal body, doc.to_html
end

def test_not_emojify_in_pre_tags
body = '<pre>:shipit:</pre>'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com' })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_equal body, doc.to_html
end

def test_not_emojify_in_custom_single_tag_foo
body = '<foo>:shipit:</foo>'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com', ignored_ancestor_tags: %w[foo] })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com', ignored_ancestor_tags: %w[foo] })
doc = filter.call
assert_equal body, doc.to_html
end

def test_not_emojify_in_custom_multiple_tags_foo_and_bar
body = '<bar>:shipit:</bar>'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com', ignored_ancestor_tags: %w[foo bar] })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com', ignored_ancestor_tags: %w[foo bar] })
doc = filter.call
assert_equal body, doc.to_html
end

def test_img_tag_attributes
body = ':shipit:'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com' })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_equal %(<img class="emoji" title=":shipit:" alt=":shipit:" src="https://foo.com/emoji/shipit.png" height="20" width="20" align="absmiddle">), doc.to_html
end

def test_img_tag_attributes_can_be_customized
body = ':shipit:'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com', img_attrs: Hash('draggable' => 'false', 'height' => nil, 'width' => nil, 'align' => nil) })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com', img_attrs: Hash('draggable' => 'false', 'height' => nil, 'width' => nil, 'align' => nil) })
doc = filter.call
assert_equal %(<img class="emoji" title=":shipit:" alt=":shipit:" src="https://foo.com/emoji/shipit.png" draggable="false">), doc.to_html
end

def test_img_attrs_value_can_accept_proclike_object
remove_colons = ->(name) { name.delete(':') }
body = ':shipit:'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com', img_attrs: Hash('title' => remove_colons) })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com', img_attrs: Hash('title' => remove_colons) })
doc = filter.call
assert_equal %(<img class="emoji" title="shipit" alt=":shipit:" src="https://foo.com/emoji/shipit.png" height="20" width="20" align="absmiddle">), doc.to_html
end

def test_img_attrs_can_accept_symbolized_keys
body = ':shipit:'
filter = EmojiFilter.new(body, context: { asset_root: 'https://foo.com', img_attrs: Hash(draggable: false, height: nil, width: nil, align: nil) })
filter = @emoji_filter.new(body, context: { asset_root: 'https://foo.com', img_attrs: Hash(draggable: false, height: nil, width: nil, align: nil) })
doc = filter.call
assert_equal %(<img class="emoji" title=":shipit:" alt=":shipit:" src="https://foo.com/emoji/shipit.png" draggable="false">), doc.to_html
end

def test_works_with_gemoji
require 'gemojione'

HTML::Pipeline::EmojiFilter.stub :gemoji_loaded?, false do
body = ':flag_ar:'
filter = HTML::Pipeline::EmojiFilter.new(body, context: { asset_root: 'https://foo.com' })
doc = filter.call
assert_equal %(<img class="emoji" title=":flag_ar:" alt=":flag_ar:" src="https://foo.com/emoji/1f1e6-1f1f7.png" height="20" width="20" align="absmiddle">), doc.to_html
end
end

def test_gemoji_can_accept_symbolized_keys
require 'gemojione'
HTML::Pipeline::EmojiFilter.stub :gemoji_loaded?, false do
body = ':flag_ar:'
filter = HTML::Pipeline::EmojiFilter.new(body, context: { asset_root: 'https://coolwebsite.com', img_attrs: Hash(draggable: false, height: nil, width: nil, align: nil) })
doc = filter.call
assert_equal %(<img class="emoji" title=":flag_ar:" alt=":flag_ar:" src="https://coolwebsite.com/emoji/1f1e6-1f1f7.png" draggable="false">), doc.to_html
end
end
end
23 changes: 21 additions & 2 deletions test/html/pipeline/require_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,42 @@
require 'test_helper'

class HTML::Pipeline::RequireHelperTest < Minitest::Test
def test_works_with_existing
def test_works_with_existing_dependency
HTML::Pipeline.require_dependency('rake', 'SomeClass')
end

def test_works_with_existing_dependencies
HTML::Pipeline.require_dependencies(%w[old_sql nokogiri], 'SomeClass')
assert HTML::Pipeline.nokogiri_loaded?
refute HTML::Pipeline.old_sql_loaded?
end

def test_raises_mising_dependency_error
assert_raises HTML::Pipeline::MissingDependencyError do
HTML::Pipeline.require_dependency('non-existant', 'SomeClass')
end
end

def test_raises_error_including_message
def test_raises_mising_dependenccies_error
assert_raises HTML::Pipeline::MissingDependencyError do
HTML::Pipeline.require_dependencies(%w[non-existant something], 'SomeClass')
end
end

def test_raises_dependency_error_including_message
error = assert_raises(HTML::Pipeline::MissingDependencyError) do
HTML::Pipeline.require_dependency('non-existant', 'SomeClass')
end
assert_includes(error.message, "Missing dependency 'non-existant' for SomeClass. See README.md for details.")
end

def test_raises_dependencies_error_including_message
error = assert_raises(HTML::Pipeline::MissingDependencyError) do
HTML::Pipeline.require_dependencies(%w[non-existant something], 'SomeClass')
end
assert_includes(error.message, "Missing all dependencies 'non-existant, something' for SomeClass. See README.md for details.")
end

def test_raises_error_includes_underlying_message
error = assert_raises HTML::Pipeline::MissingDependencyError do
HTML::Pipeline.require_dependency('non-existant', 'SomeClass')
Expand Down
3 changes: 3 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

require 'bundler/setup'
require 'html/pipeline'

require 'minitest/autorun'
require 'minitest/pride'
require 'minitest/focus'

require 'awesome_print'

module TestHelpers
# Asserts that two html fragments are equivalent. Attribute order
# will be ignored.
Expand Down

0 comments on commit 4745c88

Please sign in to comment.