motion-html-pipeline 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +379 -0
  3. data/lib/motion-html-pipeline.rb +14 -0
  4. data/lib/motion-html-pipeline/document_fragment.rb +27 -0
  5. data/lib/motion-html-pipeline/pipeline.rb +153 -0
  6. data/lib/motion-html-pipeline/pipeline/absolute_source_filter.rb +45 -0
  7. data/lib/motion-html-pipeline/pipeline/body_content.rb +42 -0
  8. data/lib/motion-html-pipeline/pipeline/disabled/@mention_filter.rb +140 -0
  9. data/lib/motion-html-pipeline/pipeline/disabled/autolink_filter.rb +27 -0
  10. data/lib/motion-html-pipeline/pipeline/disabled/camo_filter.rb +93 -0
  11. data/lib/motion-html-pipeline/pipeline/disabled/email_reply_filter.rb +66 -0
  12. data/lib/motion-html-pipeline/pipeline/disabled/emoji_filter.rb +125 -0
  13. data/lib/motion-html-pipeline/pipeline/disabled/markdown_filter.rb +37 -0
  14. data/lib/motion-html-pipeline/pipeline/disabled/plain_text_input_filter.rb +13 -0
  15. data/lib/motion-html-pipeline/pipeline/disabled/sanitization_filter.rb +137 -0
  16. data/lib/motion-html-pipeline/pipeline/disabled/syntax_highlight_filter.rb +44 -0
  17. data/lib/motion-html-pipeline/pipeline/disabled/toc_filter.rb +67 -0
  18. data/lib/motion-html-pipeline/pipeline/filter.rb +163 -0
  19. data/lib/motion-html-pipeline/pipeline/https_filter.rb +27 -0
  20. data/lib/motion-html-pipeline/pipeline/image_filter.rb +17 -0
  21. data/lib/motion-html-pipeline/pipeline/image_max_width_filter.rb +37 -0
  22. data/lib/motion-html-pipeline/pipeline/text_filter.rb +14 -0
  23. data/lib/motion-html-pipeline/pipeline/version.rb +5 -0
  24. data/spec/motion-html-pipeline/_helpers/mock_instumentation_service.rb +19 -0
  25. data/spec/motion-html-pipeline/pipeline/absolute_source_filter_spec.rb +47 -0
  26. data/spec/motion-html-pipeline/pipeline/disabled/auto_link_filter_spec.rb +33 -0
  27. data/spec/motion-html-pipeline/pipeline/disabled/camo_filter_spec.rb +75 -0
  28. data/spec/motion-html-pipeline/pipeline/disabled/email_reply_filter_spec.rb +64 -0
  29. data/spec/motion-html-pipeline/pipeline/disabled/emoji_filter_spec.rb +92 -0
  30. data/spec/motion-html-pipeline/pipeline/disabled/markdown_filter_spec.rb +112 -0
  31. data/spec/motion-html-pipeline/pipeline/disabled/plain_text_input_filter_spec.rb +20 -0
  32. data/spec/motion-html-pipeline/pipeline/disabled/sanitization_filter_spec.rb +164 -0
  33. data/spec/motion-html-pipeline/pipeline/disabled/syntax_highlighting_filter_spec.rb +59 -0
  34. data/spec/motion-html-pipeline/pipeline/disabled/toc_filter_spec.rb +137 -0
  35. data/spec/motion-html-pipeline/pipeline/https_filter_spec.rb +52 -0
  36. data/spec/motion-html-pipeline/pipeline/image_filter_spec.rb +37 -0
  37. data/spec/motion-html-pipeline/pipeline/image_max_width_filter_spec.rb +57 -0
  38. data/spec/motion-html-pipeline/pipeline_spec.rb +80 -0
  39. data/spec/spec_helper.rb +48 -0
  40. metadata +147 -0
@@ -0,0 +1,27 @@
1
+ module MotionHTMLPipeline
2
+ class Pipeline
3
+ # HTML Filter for replacing http references to :http_url with https versions.
4
+ # Subdomain references are not rewritten.
5
+ #
6
+ # Context options:
7
+ # :http_url - The HTTP url to force HTTPS. Falls back to :base_url
8
+ class HttpsFilter < Filter
9
+ def call
10
+ doc.css(%(a[href^="#{http_url}"])).each do |element|
11
+ element['href'] = element['href'].sub(/^http:/, 'https:')
12
+ end
13
+ doc
14
+ end
15
+
16
+ # HTTP url to replace. Falls back to :base_url
17
+ def http_url
18
+ context[:http_url] || context[:base_url]
19
+ end
20
+
21
+ # Raise error if :http_url undefined
22
+ def validate
23
+ needs :http_url unless http_url
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ module MotionHTMLPipeline
2
+ class Pipeline
3
+ # HTML Filter that converts image's url into <img> tag.
4
+ # For example, it will convert
5
+ # http://example.com/test.jpg
6
+ # into
7
+ # <img src="http://example.com/test.jpg" alt=""/>.
8
+
9
+ class ImageFilter < TextFilter
10
+ def call
11
+ @text.gsub(/(https|http)?:\/\/.+\.(jpg|jpeg|bmp|gif|png)(\?\S+)?/i) do |match|
12
+ %(<img src="#{match}" alt=""/>)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ module MotionHTMLPipeline
2
+ class Pipeline
3
+ # This filter rewrites image tags with a max-width inline style and also wraps
4
+ # the image in an <a> tag that causes the full size image to be opened in a
5
+ # new tab.
6
+ #
7
+ # The max-width inline styles are especially useful in HTML email which
8
+ # don't use a global stylesheets.
9
+ class ImageMaxWidthFilter < Filter
10
+ def call
11
+ doc.css('img').each do |element|
12
+ # Skip if there's already a style attribute. Not sure how this
13
+ # would happen but we can reconsider it in the future.
14
+ next if element['style']
15
+
16
+ # Bail out if src doesn't look like a valid http url. trying to avoid weird
17
+ # js injection via javascript: urls.
18
+ next if element['src'].to_s.strip =~ /\Ajavascript/i
19
+
20
+ element['style'] = 'max-width:100%;'
21
+
22
+ link_image element unless has_ancestor?(element, %w[a])
23
+ end
24
+
25
+ doc
26
+ end
27
+
28
+ def link_image(element)
29
+ link = HTMLElement.alloc.initWithTagName('a', attributes: { href: element['src'], target: '_blank' })
30
+ link.appendNode(element.cloneNodeDeep(true))
31
+
32
+ parent = element.parentNode
33
+ parent.replaceChildNode(element, withNode: link)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module MotionHTMLPipeline
2
+ class Pipeline
3
+ class TextFilter < Filter
4
+ attr_reader :text
5
+
6
+ def initialize(text, context = nil, result = nil)
7
+ raise TypeError, 'text cannot be HTML' if text.is_a?(DocumentFragment)
8
+ # Ensure that this is always a string
9
+ @text = text.respond_to?(:to_str) ? text.to_str : text.to_s
10
+ super nil, context, result
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module MotionHTMLPipeline
2
+ class Pipeline
3
+ VERSION = '0.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ class MockedInstrumentationService
2
+ attr_reader :events
3
+ def initialize(event = nil, events = [])
4
+ @events = events
5
+ subscribe event
6
+ end
7
+
8
+ def instrument(event, payload = nil)
9
+ payload ||= {}
10
+ res = yield payload
11
+ events << [event, payload, res] if @subscribe == event
12
+ res
13
+ end
14
+
15
+ def subscribe(event)
16
+ @subscribe = event
17
+ @events
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ describe 'MotionHTMLPipeline::Pipeline::AbsoluteSourceFilterTest' do
2
+ AbsoluteSourceFilter = MotionHTMLPipeline::Pipeline::AbsoluteSourceFilter
3
+
4
+ before do
5
+ @image_base_url = 'http://assets.example.com'
6
+ @image_subpage_url = 'http://blog.example.com/a/post'
7
+ @options = {
8
+ image_base_url: @image_base_url,
9
+ image_subpage_url: @image_subpage_url
10
+ }
11
+ end
12
+
13
+ it 'test_rewrites_root_urls' do
14
+ orig = %(<p><img src="/img.png"></p>)
15
+
16
+ expect("<p><img src=\"#{@image_base_url}/img.png\"></p>")
17
+ .to eq AbsoluteSourceFilter.call(orig, @options).to_s
18
+ end
19
+
20
+ it 'test_rewrites_relative_urls' do
21
+ orig = %(<p><img src="post/img.png"></p>)
22
+
23
+ expect("<p><img src=\"#{@image_subpage_url}/img.png\"></p>")
24
+ .to eq AbsoluteSourceFilter.call(orig, @options).to_s
25
+ end
26
+
27
+ it 'test_does_not_rewrite_absolute_urls' do
28
+ orig = %(<p><img src="http://other.example.com/img.png"></p>)
29
+ result = AbsoluteSourceFilter.call(orig, @options).to_s
30
+
31
+ expect(result).not_to match(/@image_base_url/)
32
+ expect(result).not_to match(/@@image_subpage_url/)
33
+ end
34
+
35
+ it 'test_fails_when_context_is_missing' do
36
+ expect{ AbsoluteSourceFilter.call('<img src="img.png">', {}) }.to raise_error(RuntimeError)
37
+ expect{ AbsoluteSourceFilter.call('<img src="/img.png">', {}) }.to raise_error(RuntimeError)
38
+ end
39
+
40
+ it 'test_tells_you_where_context_is_required' do
41
+ expect{ AbsoluteSourceFilter.call('<img src="img.png">', {}) }
42
+ .to raise_error(RuntimeError, 'MotionHTMLPipeline::Pipeline::AbsoluteSourceFilter')
43
+
44
+ expect{ AbsoluteSourceFilter.call('<img src="/img.png">', {}) }
45
+ .to raise_error(RuntimeError, 'MotionHTMLPipeline::Pipeline::AbsoluteSourceFilter')
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ # describe 'MotionHTMLPipeline::Pipeline::AutolinkFilterTest' do
2
+ # AutolinkFilter = MotionHTMLPipeline::Pipeline::AutolinkFilter
3
+ #
4
+ # it '#test_uses_rinku_for_autolinking' do
5
+ # # just try to parse a complicated piece of HTML
6
+ # # that Rails auto_link cannot handle
7
+ # expect('<p>"<a href="http://www.github.com">http://www.github.com</a>"</p>')
8
+ # .to eq AutolinkFilter.to_html('<p>"http://www.github.com"</p>')
9
+ # end
10
+ #
11
+ # def test_autolink_option
12
+ # assert_equal '<p>"http://www.github.com"</p>',
13
+ # AutolinkFilter.to_html('<p>"http://www.github.com"</p>', autolink: false)
14
+ # end
15
+ #
16
+ # def test_autolink_link_attr
17
+ # assert_equal '<p>"<a href="http://www.github.com" target="_blank">http://www.github.com</a>"</p>',
18
+ # AutolinkFilter.to_html('<p>"http://www.github.com"</p>', link_attr: 'target="_blank"')
19
+ # end
20
+ #
21
+ # def test_autolink_flags
22
+ # assert_equal '<p>"<a href="http://github">http://github</a>"</p>',
23
+ # AutolinkFilter.to_html('<p>"http://github"</p>', flags: Rinku::AUTOLINK_SHORT_DOMAINS)
24
+ # end
25
+ #
26
+ # def test_autolink_skip_tags
27
+ # assert_equal '<code>"http://github.com"</code>',
28
+ # AutolinkFilter.to_html('<code>"http://github.com"</code>')
29
+ #
30
+ # assert_equal '<code>"<a href="http://github.com">http://github.com</a>"</code>',
31
+ # AutolinkFilter.to_html('<code>"http://github.com"</code>', skip_tags: %w[kbd script])
32
+ # end
33
+ # end
@@ -0,0 +1,75 @@
1
+ # describe 'MotionHTMLPipeline::Pipeline::CamoFilterTest' do
2
+ # CamoFilter = MotionHTMLPipeline::Pipeline::CamoFilter
3
+ #
4
+ # def setup
5
+ # @asset_proxy_url = 'https//assets.example.org'
6
+ # @asset_proxy_secret_key = 'ssssh-secret'
7
+ # @options = {
8
+ # asset_proxy: @asset_proxy_url,
9
+ # asset_proxy_secret_key: @asset_proxy_secret_key,
10
+ # asset_proxy_whitelist: [/(^|\.)github\.com$/]
11
+ # }
12
+ # end
13
+ #
14
+ # def test_asset_proxy_disabled
15
+ # orig = %(<p><img src="http://twitter.com/img.png"></p>)
16
+ # assert_equal orig,
17
+ # CamoFilter.call(orig, @options.merge(disable_asset_proxy: true)).to_s
18
+ # end
19
+ #
20
+ # def test_camouflaging_http_image_urls
21
+ # orig = %(<p><img src="http://twitter.com/img.png"></p>)
22
+ # assert_equal %(<p><img src="https//assets.example.org/a5ad43494e343b20d745586282be61ff530e6fa0/687474703a2f2f747769747465722e636f6d2f696d672e706e67" data-canonical-src="http://twitter.com/img.png"></p>),
23
+ # CamoFilter.call(orig, @options).to_s
24
+ # end
25
+ #
26
+ # def test_doesnt_rewrite_dotcom_image_urls
27
+ # orig = %(<p><img src="https://github.com/img.png"></p>)
28
+ # assert_equal orig, CamoFilter.call(orig, @options).to_s
29
+ # end
30
+ #
31
+ # def test_doesnt_rewrite_dotcom_subdomain_image_urls
32
+ # orig = %(<p><img src="https://raw.github.com/img.png"></p>)
33
+ # assert_equal orig, CamoFilter.call(orig, @options).to_s
34
+ # end
35
+ #
36
+ # def test_doesnt_rewrite_dotcom_subsubdomain_image_urls
37
+ # orig = %(<p><img src="https://f.assets.github.com/img.png"></p>)
38
+ # assert_equal orig, CamoFilter.call(orig, @options).to_s
39
+ # end
40
+ #
41
+ # def test_camouflaging_github_prefixed_image_urls
42
+ # orig = %(<p><img src="https://notgithub.com/img.png"></p>)
43
+ # assert_equal %(<p><img src="https//assets.example.org/5d4a96c69713f850520538e04cb9661035cfb534/68747470733a2f2f6e6f746769746875622e636f6d2f696d672e706e67" data-canonical-src="https://notgithub.com/img.png"></p>),
44
+ # CamoFilter.call(orig, @options).to_s
45
+ # end
46
+ #
47
+ # def test_doesnt_rewrite_absolute_image_urls
48
+ # orig = %(<p><img src="/img.png"></p>)
49
+ # assert_equal orig, CamoFilter.call(orig, @options).to_s
50
+ # end
51
+ #
52
+ # def test_doesnt_rewrite_relative_image_urls
53
+ # orig = %(<p><img src="img.png"></p>)
54
+ # assert_equal orig, CamoFilter.call(orig, @options).to_s
55
+ # end
56
+ #
57
+ # def test_camouflaging_https_image_urls
58
+ # orig = %(<p><img src="https://foo.com/img.png"></p>)
59
+ # assert_equal %(<p><img src="https//assets.example.org/3c5c6dc74fd6592d2596209dfcb8b7e5461383c8/68747470733a2f2f666f6f2e636f6d2f696d672e706e67" data-canonical-src="https://foo.com/img.png"></p>),
60
+ # CamoFilter.call(orig, @options).to_s
61
+ # end
62
+ #
63
+ # def test_handling_images_with_no_src_attribute
64
+ # orig = %(<p><img></p>)
65
+ # assert_equal orig, CamoFilter.call(orig, @options).to_s
66
+ # end
67
+ #
68
+ # def test_required_context_validation
69
+ # exception = assert_raises(ArgumentError) do
70
+ # CamoFilter.call('', {})
71
+ # end
72
+ # assert_match /:asset_proxy[^_]/, exception.message
73
+ # assert_match /:asset_proxy_secret_key/, exception.message
74
+ # end
75
+ # end
@@ -0,0 +1,64 @@
1
+ # describe 'MotionHTMLPipeline::Pipeline::EmailReplyFilterTest' do
2
+ # EmailReplyFilter = MotionHTMLPipeline::Pipeline::EmailReplyFilter
3
+ #
4
+ # def setup
5
+ # @body = <<-EMAIL
6
+ # Hey, don't send email addresses in comments. They aren't filtered.
7
+ #
8
+ # > On Mar 5, 2016, at 08:05, Boaty McBoatface <boatymcboatface@example.com> wrote:
9
+ # >
10
+ # > Sup. alreadyleaked@example.com
11
+ # >
12
+ # > —
13
+ # > Reply to this email directly or view it on GitHub.
14
+ # EMAIL
15
+ # end
16
+ #
17
+ # def test_doesnt_hide_by_default
18
+ # filter = EmailReplyFilter.new(@body)
19
+ # doc = filter.call.to_s
20
+ # assert_match /alreadyleaked@example.com/, doc
21
+ # assert_match /boatymcboatface@example.com/, doc
22
+ # end
23
+ #
24
+ # def test_hides_email_addresses_when_configured
25
+ # filter = EmailReplyFilter.new(@body, hide_quoted_email_addresses: true)
26
+ # doc = filter.call.to_s
27
+ # refute_match /boatymcboatface@example.com/, doc
28
+ # refute_match /alreadyleaked@example.com/, doc
29
+ # end
30
+ #
31
+ # def test_preserves_non_email_content_while_filtering
32
+ # str = <<-EMAIL
33
+ # > Thank you! I have some thoughts on this pull request.
34
+ # >
35
+ # > * acme provides cmake and a wrapper for it. Please use '$(TARGET)-cmake' instead of cmake -DCMAKE_TOOLCHAIN_FILE='$(CMAKE_TOOLCHAIN_FILE)' -DCMAKE_BUILD_TYPE=Release.
36
+ #
37
+ # Okay -- I'm afraid I just blindly copied the eigen3.mk file, since that's a library I'm familiar with :-)
38
+ #
39
+ # > * Do you need -DCMAKE_SYSTEM_PROCESSOR=x86?
40
+ #
41
+ # Yes, this is a bit dumb, but vc checks for that (or amd) to determine that it's not being built on ARM.
42
+ #
43
+ # --
44
+ # Boaty McBoatface | http://example.org
45
+ # EMAIL
46
+ #
47
+ # filter = EmailReplyFilter.new(str, hide_quoted_email_addresses: true)
48
+ # doc = filter.call.to_s
49
+ #
50
+ # expected = <<-EXPECTED
51
+ # <div class="email-quoted-reply"> Thank you! I have some thoughts on this pull request.
52
+ #
53
+ # * acme provides cmake and a wrapper for it. Please use &#39;$(TARGET)-cmake&#39; instead of cmake -DCMAKE_TOOLCHAIN_FILE=&#39;$(CMAKE_TOOLCHAIN_FILE)&#39; -DCMAKE_BUILD_TYPE=Release.</div>
54
+ # <div class="email-fragment">Okay -- I&#39;m afraid I just blindly copied the eigen3.mk file, since that&#39;s a library I&#39;m familiar with :-)</div>
55
+ # <div class="email-quoted-reply"> * Do you need -DCMAKE_SYSTEM_PROCESSOR=x86?</div>
56
+ # <div class="email-fragment">Yes, this is a bit dumb, but vc checks for that (or amd) to determine that it&#39;s not being built on ARM.</div>
57
+ # <span class="email-hidden-toggle"><a href="#">&hellip;</a></span><div class="email-hidden-reply" style="display:none"><div class="email-signature-reply">--
58
+ # Boaty McBoatface | http:&#47;&#47;example.org</div>
59
+ # </div>
60
+ # EXPECTED
61
+ #
62
+ # assert_equal(expected.chomp, doc)
63
+ # end
64
+ # end
@@ -0,0 +1,92 @@
1
+ # describe 'MotionHTMLPipeline::Pipeline::EmojiFilterTest' do
2
+ # EmojiFilter = MotionHTMLPipeline::Pipeline::EmojiFilter
3
+ #
4
+ # def test_emojify
5
+ # filter = EmojiFilter.new('<p>:shipit:</p>', asset_root: 'https://foo.com')
6
+ # doc = filter.call
7
+ # assert_match 'https://foo.com/emoji/shipit.png', doc.search('img').attr('src').value
8
+ # end
9
+ #
10
+ # def test_uri_encoding
11
+ # filter = EmojiFilter.new('<p>:+1:</p>', asset_root: 'https://foo.com')
12
+ # doc = filter.call
13
+ # assert_match 'https://foo.com/emoji/unicode/1f44d.png', doc.search('img').attr('src').value
14
+ # end
15
+ #
16
+ # def test_required_context_validation
17
+ # exception = assert_raises(ArgumentError) do
18
+ # EmojiFilter.call('', {})
19
+ # end
20
+ # assert_match /:asset_root/, exception.message
21
+ # end
22
+ #
23
+ # def test_custom_asset_path
24
+ # filter = EmojiFilter.new('<p>:+1:</p>', asset_path: ':file_name', asset_root: 'https://foo.com')
25
+ # doc = filter.call
26
+ # assert_match 'https://foo.com/unicode/1f44d.png', doc.search('img').attr('src').value
27
+ # end
28
+ #
29
+ # def test_not_emojify_in_code_tags
30
+ # body = '<code>:shipit:</code>'
31
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com')
32
+ # doc = filter.call
33
+ # assert_equal body, doc.to_html
34
+ # end
35
+ #
36
+ # def test_not_emojify_in_tt_tags
37
+ # body = '<tt>:shipit:</tt>'
38
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com')
39
+ # doc = filter.call
40
+ # assert_equal body, doc.to_html
41
+ # end
42
+ #
43
+ # def test_not_emojify_in_pre_tags
44
+ # body = '<pre>:shipit:</pre>'
45
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com')
46
+ # doc = filter.call
47
+ # assert_equal body, doc.to_html
48
+ # end
49
+ #
50
+ # def test_not_emojify_in_custom_single_tag_foo
51
+ # body = '<foo>:shipit:</foo>'
52
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com', ignored_ancestor_tags: %w[foo])
53
+ # doc = filter.call
54
+ # assert_equal body, doc.to_html
55
+ # end
56
+ #
57
+ # def test_not_emojify_in_custom_multiple_tags_foo_and_bar
58
+ # body = '<bar>:shipit:</bar>'
59
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com', ignored_ancestor_tags: %w[foo bar])
60
+ # doc = filter.call
61
+ # assert_equal body, doc.to_html
62
+ # end
63
+ #
64
+ # def test_img_tag_attributes
65
+ # body = ':shipit:'
66
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com')
67
+ # doc = filter.call
68
+ # 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
69
+ # end
70
+ #
71
+ # def test_img_tag_attributes_can_be_customized
72
+ # body = ':shipit:'
73
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com', img_attrs: Hash('draggable' => 'false', 'height' => nil, 'width' => nil, 'align' => nil))
74
+ # doc = filter.call
75
+ # assert_equal %(<img class="emoji" title=":shipit:" alt=":shipit:" src="https://foo.com/emoji/shipit.png" draggable="false">), doc.to_html
76
+ # end
77
+ #
78
+ # def test_img_attrs_value_can_accept_proclike_object
79
+ # remove_colons = ->(name) { name.delete(':') }
80
+ # body = ':shipit:'
81
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com', img_attrs: Hash('title' => remove_colons))
82
+ # doc = filter.call
83
+ # 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
84
+ # end
85
+ #
86
+ # def test_img_attrs_can_accept_symbolized_keys
87
+ # body = ':shipit:'
88
+ # filter = EmojiFilter.new(body, asset_root: 'https://foo.com', img_attrs: Hash(draggable: false, height: nil, width: nil, align: nil))
89
+ # doc = filter.call
90
+ # assert_equal %(<img class="emoji" title=":shipit:" alt=":shipit:" src="https://foo.com/emoji/shipit.png" draggable="false">), doc.to_html
91
+ # end
92
+ # end