motion-html-pipeline 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +379 -0
- data/lib/motion-html-pipeline.rb +14 -0
- data/lib/motion-html-pipeline/document_fragment.rb +27 -0
- data/lib/motion-html-pipeline/pipeline.rb +153 -0
- data/lib/motion-html-pipeline/pipeline/absolute_source_filter.rb +45 -0
- data/lib/motion-html-pipeline/pipeline/body_content.rb +42 -0
- data/lib/motion-html-pipeline/pipeline/disabled/@mention_filter.rb +140 -0
- data/lib/motion-html-pipeline/pipeline/disabled/autolink_filter.rb +27 -0
- data/lib/motion-html-pipeline/pipeline/disabled/camo_filter.rb +93 -0
- data/lib/motion-html-pipeline/pipeline/disabled/email_reply_filter.rb +66 -0
- data/lib/motion-html-pipeline/pipeline/disabled/emoji_filter.rb +125 -0
- data/lib/motion-html-pipeline/pipeline/disabled/markdown_filter.rb +37 -0
- data/lib/motion-html-pipeline/pipeline/disabled/plain_text_input_filter.rb +13 -0
- data/lib/motion-html-pipeline/pipeline/disabled/sanitization_filter.rb +137 -0
- data/lib/motion-html-pipeline/pipeline/disabled/syntax_highlight_filter.rb +44 -0
- data/lib/motion-html-pipeline/pipeline/disabled/toc_filter.rb +67 -0
- data/lib/motion-html-pipeline/pipeline/filter.rb +163 -0
- data/lib/motion-html-pipeline/pipeline/https_filter.rb +27 -0
- data/lib/motion-html-pipeline/pipeline/image_filter.rb +17 -0
- data/lib/motion-html-pipeline/pipeline/image_max_width_filter.rb +37 -0
- data/lib/motion-html-pipeline/pipeline/text_filter.rb +14 -0
- data/lib/motion-html-pipeline/pipeline/version.rb +5 -0
- data/spec/motion-html-pipeline/_helpers/mock_instumentation_service.rb +19 -0
- data/spec/motion-html-pipeline/pipeline/absolute_source_filter_spec.rb +47 -0
- data/spec/motion-html-pipeline/pipeline/disabled/auto_link_filter_spec.rb +33 -0
- data/spec/motion-html-pipeline/pipeline/disabled/camo_filter_spec.rb +75 -0
- data/spec/motion-html-pipeline/pipeline/disabled/email_reply_filter_spec.rb +64 -0
- data/spec/motion-html-pipeline/pipeline/disabled/emoji_filter_spec.rb +92 -0
- data/spec/motion-html-pipeline/pipeline/disabled/markdown_filter_spec.rb +112 -0
- data/spec/motion-html-pipeline/pipeline/disabled/plain_text_input_filter_spec.rb +20 -0
- data/spec/motion-html-pipeline/pipeline/disabled/sanitization_filter_spec.rb +164 -0
- data/spec/motion-html-pipeline/pipeline/disabled/syntax_highlighting_filter_spec.rb +59 -0
- data/spec/motion-html-pipeline/pipeline/disabled/toc_filter_spec.rb +137 -0
- data/spec/motion-html-pipeline/pipeline/https_filter_spec.rb +52 -0
- data/spec/motion-html-pipeline/pipeline/image_filter_spec.rb +37 -0
- data/spec/motion-html-pipeline/pipeline/image_max_width_filter_spec.rb +57 -0
- data/spec/motion-html-pipeline/pipeline_spec.rb +80 -0
- data/spec/spec_helper.rb +48 -0
- metadata +147 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module MotionHTMLPipeline
|
2
|
+
class Pipeline
|
3
|
+
class AbsoluteSourceFilter < Filter
|
4
|
+
# HTML Filter for replacing relative and root relative image URLs with
|
5
|
+
# fully qualified URLs
|
6
|
+
#
|
7
|
+
# This is useful if an image is root relative but should really be going
|
8
|
+
# through a cdn, or if the content for the page assumes the host is known
|
9
|
+
# i.e. scraped webpages and some RSS feeds.
|
10
|
+
#
|
11
|
+
# Context options:
|
12
|
+
# :image_base_url - Base URL for image host for root relative src.
|
13
|
+
# :image_subpage_url - For relative src.
|
14
|
+
#
|
15
|
+
# This filter does not write additional information to the context.
|
16
|
+
# This filter would need to be run before CamoFilter.
|
17
|
+
def call
|
18
|
+
doc.css('img').each do |element|
|
19
|
+
next if element['src'].nil? || element['src'].empty?
|
20
|
+
src = element['src'].strip
|
21
|
+
next if src.start_with? 'http'
|
22
|
+
base = if src.start_with? '/'
|
23
|
+
image_base_url
|
24
|
+
else
|
25
|
+
image_subpage_url
|
26
|
+
end
|
27
|
+
|
28
|
+
base = NSURL.URLWithString(base)
|
29
|
+
element['src'] = NSURL.URLWithString(src, relativeToURL: base).absoluteString
|
30
|
+
end
|
31
|
+
doc
|
32
|
+
end
|
33
|
+
|
34
|
+
# Private: the base url you want to use
|
35
|
+
def image_base_url
|
36
|
+
context[:image_base_url] || raise("Missing context :image_base_url for #{self.class.name}")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Private: the relative url you want to use
|
40
|
+
def image_subpage_url
|
41
|
+
context[:image_subpage_url] || raise("Missing context :image_subpage_url for #{self.class.name}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module MotionHTMLPipeline
|
2
|
+
class Pipeline
|
3
|
+
# Public: Runs a String of content through an HTML processing pipeline,
|
4
|
+
# providing easy access to a generated DocumentFragment.
|
5
|
+
class BodyContent
|
6
|
+
attr_reader :result
|
7
|
+
|
8
|
+
# Public: Initialize a BodyContent.
|
9
|
+
#
|
10
|
+
# body - A String body.
|
11
|
+
# context - A Hash of context options for the filters.
|
12
|
+
# pipeline - A MotionHTMLPipeline::Pipeline object with one or more Filters.
|
13
|
+
def initialize(body, context, pipeline)
|
14
|
+
@body = body
|
15
|
+
@context = context
|
16
|
+
@pipeline = pipeline
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Gets the memoized result of the body content as it passed through
|
20
|
+
# the Pipeline.
|
21
|
+
#
|
22
|
+
# Returns a Hash, or something similar as defined by @pipeline.result_class.
|
23
|
+
def result
|
24
|
+
@result ||= @pipeline.call @body, @context
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Gets the updated body from the Pipeline result.
|
28
|
+
#
|
29
|
+
# Returns a String or DocumentFragment.
|
30
|
+
def output
|
31
|
+
@output ||= result[:output]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Parses the output into a DocumentFragment.
|
35
|
+
#
|
36
|
+
# Returns a DocumentFragment.
|
37
|
+
def document
|
38
|
+
@document ||= MotionHTMLPipeline::Pipeline.parse output
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# TODO Requires Set (or something similar)
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
# require 'set'
|
4
|
+
#
|
5
|
+
# module MotionHTMLPipeline
|
6
|
+
# class Pipeline
|
7
|
+
# # HTML filter that replaces @user mentions with links. Mentions within <pre>,
|
8
|
+
# # <code>, and <a> elements are ignored. Mentions that reference users that do
|
9
|
+
# # not exist are ignored.
|
10
|
+
# #
|
11
|
+
# # Context options:
|
12
|
+
# # :base_url - Used to construct links to user profile pages for each
|
13
|
+
# # mention.
|
14
|
+
# # :info_url - Used to link to "more info" when someone mentions @mention
|
15
|
+
# # or @mentioned.
|
16
|
+
# # :username_pattern - Used to provide a custom regular expression to
|
17
|
+
# # identify usernames
|
18
|
+
# #
|
19
|
+
# class MentionFilter < Filter
|
20
|
+
# # Public: Find user @mentions in text. See
|
21
|
+
# # MentionFilter#mention_link_filter.
|
22
|
+
# #
|
23
|
+
# # MentionFilter.mentioned_logins_in(text) do |match, login, is_mentioned|
|
24
|
+
# # "<a href=...>#{login}</a>"
|
25
|
+
# # end
|
26
|
+
# #
|
27
|
+
# # text - String text to search.
|
28
|
+
# #
|
29
|
+
# # Yields the String match, the String login name, and a Boolean determining
|
30
|
+
# # if the match = "@mention[ed]". The yield's return replaces the match in
|
31
|
+
# # the original text.
|
32
|
+
# #
|
33
|
+
# # Returns a String replaced with the return of the block.
|
34
|
+
# def self.mentioned_logins_in(text, username_pattern = UsernamePattern)
|
35
|
+
# text.gsub MentionPatterns[username_pattern] do |match|
|
36
|
+
# login = Regexp.last_match(1)
|
37
|
+
# yield match, login, MentionLogins.include?(login.downcase)
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# # Hash that contains all of the mention patterns used by the pipeline
|
42
|
+
# MentionPatterns = Hash.new do |hash, key|
|
43
|
+
# hash[key] = /
|
44
|
+
# (?:^|\W) # beginning of string or non-word char
|
45
|
+
# @((?>#{key})) # @username
|
46
|
+
# (?!\/) # without a trailing slash
|
47
|
+
# (?=
|
48
|
+
# \.+[ \t\W]| # dots followed by space or non-word character
|
49
|
+
# \.+$| # dots at end of line
|
50
|
+
# [^0-9a-zA-Z_.]| # non-word character except dot
|
51
|
+
# $ # end of line
|
52
|
+
# )
|
53
|
+
# /ix
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# # Default pattern used to extract usernames from text. The value can be
|
57
|
+
# # overriden by providing the username_pattern variable in the context.
|
58
|
+
# UsernamePattern = /[a-z0-9][a-z0-9-]*/
|
59
|
+
#
|
60
|
+
# # List of username logins that, when mentioned, link to the blog post
|
61
|
+
# # about @mentions instead of triggering a real mention.
|
62
|
+
# MentionLogins = %w[
|
63
|
+
# mention
|
64
|
+
# mentions
|
65
|
+
# mentioned
|
66
|
+
# mentioning
|
67
|
+
# ].freeze
|
68
|
+
#
|
69
|
+
# # Don't look for mentions in text nodes that are children of these elements
|
70
|
+
# IGNORE_PARENTS = %w(pre code a style script).to_set
|
71
|
+
#
|
72
|
+
# def call
|
73
|
+
# result[:mentioned_usernames] ||= []
|
74
|
+
#
|
75
|
+
# doc.search('.//text()').each do |node|
|
76
|
+
# content = node.to_html
|
77
|
+
# next unless content.include?('@')
|
78
|
+
# next if has_ancestor?(node, IGNORE_PARENTS)
|
79
|
+
# html = mention_link_filter(content, base_url, info_url, username_pattern)
|
80
|
+
# next if html == content
|
81
|
+
# node.replace(html)
|
82
|
+
# end
|
83
|
+
# doc
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# # The URL to provide when someone @mentions a "mention" name, such
|
87
|
+
# # as @mention or @mentioned, that will give them more info on mentions.
|
88
|
+
# def info_url
|
89
|
+
# context[:info_url] || nil
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# def username_pattern
|
93
|
+
# context[:username_pattern] || UsernamePattern
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# # Replace user @mentions in text with links to the mentioned user's
|
97
|
+
# # profile page.
|
98
|
+
# #
|
99
|
+
# # text - String text to replace @mention usernames in.
|
100
|
+
# # base_url - The base URL used to construct user profile URLs.
|
101
|
+
# # info_url - The "more info" URL used to link to more info on @mentions.
|
102
|
+
# # If nil we don't link @mention or @mentioned.
|
103
|
+
# # username_pattern - Regular expression used to identify usernames in
|
104
|
+
# # text
|
105
|
+
# #
|
106
|
+
# # Returns a string with @mentions replaced with links. All links have a
|
107
|
+
# # 'user-mention' class name attached for styling.
|
108
|
+
# def mention_link_filter(text, _base_url = '/', info_url = nil, username_pattern = UsernamePattern)
|
109
|
+
# self.class.mentioned_logins_in(text, username_pattern) do |match, login, is_mentioned|
|
110
|
+
# link =
|
111
|
+
# if is_mentioned
|
112
|
+
# link_to_mention_info(login, info_url)
|
113
|
+
# else
|
114
|
+
# link_to_mentioned_user(login)
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# link ? match.sub("@#{login}", link) : match
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# def link_to_mention_info(text, info_url = nil)
|
122
|
+
# return "@#{text}" if info_url.nil?
|
123
|
+
# "<a href='#{info_url}' class='user-mention'>" \
|
124
|
+
# "@#{text}" \
|
125
|
+
# '</a>'
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# def link_to_mentioned_user(login)
|
129
|
+
# result[:mentioned_usernames] |= [login]
|
130
|
+
#
|
131
|
+
# url = base_url.dup
|
132
|
+
# url << '/' unless url =~ /[\/~]\z/
|
133
|
+
#
|
134
|
+
# "<a href='#{url << login}' class='user-mention'>" \
|
135
|
+
# "@#{login}" \
|
136
|
+
# '</a>'
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
# end
|
140
|
+
# end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# TODO Requires Rinku gem (or something similar)
|
2
|
+
#------------------------------------------------------------------------------
|
3
|
+
# module MotionHTMLPipeline
|
4
|
+
# class Pipeline
|
5
|
+
# # HTML Filter for auto_linking urls in HTML.
|
6
|
+
# #
|
7
|
+
# # Context options:
|
8
|
+
# # :autolink - boolean whether to autolink urls
|
9
|
+
# # :link_attr - HTML attributes for the link that will be generated
|
10
|
+
# # :skip_tags - HTML tags inside which autolinking will be skipped.
|
11
|
+
# # See Rinku.skip_tags
|
12
|
+
# # :flags - additional Rinku flags. See https://github.com/vmg/rinku
|
13
|
+
# #
|
14
|
+
# # This filter does not write additional information to the context.
|
15
|
+
# class AutolinkFilter < Filter
|
16
|
+
# def call
|
17
|
+
# return html if context[:autolink] == false
|
18
|
+
#
|
19
|
+
# skip_tags = context[:skip_tags]
|
20
|
+
# flags = 0
|
21
|
+
# flags |= context[:flags] if context[:flags]
|
22
|
+
#
|
23
|
+
# Rinku.auto_link(html, :urls, context[:link_attr], skip_tags, flags)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# require 'openssl'
|
2
|
+
# require 'uri'
|
3
|
+
#
|
4
|
+
# module MotionHTMLPipeline
|
5
|
+
# class Pipeline
|
6
|
+
# # HTML Filter for replacing http image URLs with camo versions. See:
|
7
|
+
# #
|
8
|
+
# # https://github.com/atmos/camo
|
9
|
+
# #
|
10
|
+
# # All images provided in user content should be run through this
|
11
|
+
# # filter so that http image sources do not cause mixed-content warnings
|
12
|
+
# # in browser clients.
|
13
|
+
# #
|
14
|
+
# # Context options:
|
15
|
+
# # :asset_proxy (required) - Base URL for constructed asset proxy URLs.
|
16
|
+
# # :asset_proxy_secret_key (required) - The shared secret used to encode URLs.
|
17
|
+
# # :asset_proxy_whitelist - Array of host Strings or Regexps to skip
|
18
|
+
# # src rewriting.
|
19
|
+
# #
|
20
|
+
# # This filter does not write additional information to the context.
|
21
|
+
# class CamoFilter < Filter
|
22
|
+
# # Hijacks images in the markup provided, replacing them with URLs that
|
23
|
+
# # go through the github asset proxy.
|
24
|
+
# def call
|
25
|
+
# return doc unless asset_proxy_enabled?
|
26
|
+
#
|
27
|
+
# doc.search('img').each do |element|
|
28
|
+
# original_src = element['src']
|
29
|
+
# next unless original_src
|
30
|
+
#
|
31
|
+
# begin
|
32
|
+
# uri = URI.parse(original_src)
|
33
|
+
# rescue Exception
|
34
|
+
# next
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# next if uri.host.nil?
|
38
|
+
# next if asset_host_whitelisted?(uri.host)
|
39
|
+
#
|
40
|
+
# element['src'] = asset_proxy_url(original_src)
|
41
|
+
# element['data-canonical-src'] = original_src
|
42
|
+
# end
|
43
|
+
# doc
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # Implementation of validate hook.
|
47
|
+
# # Errors should raise exceptions or use an existing validator.
|
48
|
+
# def validate
|
49
|
+
# needs :asset_proxy, :asset_proxy_secret_key
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# # The camouflaged URL for a given image URL.
|
53
|
+
# def asset_proxy_url(url)
|
54
|
+
# "#{asset_proxy_host}/#{asset_url_hash(url)}/#{hexencode(url)}"
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# # Private: calculate the HMAC digest for a image source URL.
|
58
|
+
# def asset_url_hash(url)
|
59
|
+
# OpenSSL::HMAC.hexdigest('sha1', asset_proxy_secret_key, url)
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# # Private: Return true if asset proxy filter should be enabled
|
63
|
+
# def asset_proxy_enabled?
|
64
|
+
# !context[:disable_asset_proxy]
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# # Private: the host to use for generated asset proxied URLs.
|
68
|
+
# def asset_proxy_host
|
69
|
+
# context[:asset_proxy]
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# def asset_proxy_secret_key
|
73
|
+
# context[:asset_proxy_secret_key]
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# def asset_proxy_whitelist
|
77
|
+
# context[:asset_proxy_whitelist] || []
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# def asset_host_whitelisted?(host)
|
81
|
+
# asset_proxy_whitelist.any? do |test|
|
82
|
+
# test.is_a?(String) ? host == test : test.match(host)
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# # Private: helper to hexencode a string. Each byte ends up encoded into
|
87
|
+
# # two characters, zero padded value in the range [0-9a-f].
|
88
|
+
# def hexencode(str)
|
89
|
+
# str.unpack('H*').first
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
# end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# MotionHTMLPipeline::Pipeline.require_dependency('escape_utils', 'EmailReplyFilter')
|
2
|
+
# MotionHTMLPipeline::Pipeline.require_dependency('email_reply_parser', 'EmailReplyFilter')
|
3
|
+
#
|
4
|
+
# module MotionHTMLPipeline
|
5
|
+
# class Pipeline
|
6
|
+
# # HTML Filter that converts email reply text into an HTML DocumentFragment.
|
7
|
+
# # It must be used as the first filter in a pipeline.
|
8
|
+
# #
|
9
|
+
# # Context options:
|
10
|
+
# # None
|
11
|
+
# #
|
12
|
+
# # This filter does not write any additional information to the context hash.
|
13
|
+
# class EmailReplyFilter < TextFilter
|
14
|
+
# include EscapeUtils
|
15
|
+
#
|
16
|
+
# EMAIL_HIDDEN_HEADER = %(<span class="email-hidden-toggle"><a href="#">…</a></span><div class="email-hidden-reply" style="display:none">).freeze
|
17
|
+
# EMAIL_QUOTED_HEADER = %(<div class="email-quoted-reply">).freeze
|
18
|
+
# EMAIL_SIGNATURE_HEADER = %(<div class="email-signature-reply">).freeze
|
19
|
+
# EMAIL_FRAGMENT_HEADER = %(<div class="email-fragment">).freeze
|
20
|
+
# EMAIL_HEADER_END = '</div>'.freeze
|
21
|
+
# EMAIL_REGEX = /[^@\s.][^@\s]*@\[?[a-z0-9.-]+\]?/
|
22
|
+
# HIDDEN_EMAIL_PATTERN = '***@***.***'.freeze
|
23
|
+
#
|
24
|
+
# # Scans an email body to determine which bits are quoted and which should
|
25
|
+
# # be hidden. EmailReplyParser is used to split the comment into an Array
|
26
|
+
# # of quoted or unquoted Blocks. Now, we loop through them and attempt to
|
27
|
+
# # add <div> tags around them so we can hide the hidden blocks, and style
|
28
|
+
# # the quoted blocks differently. Since multiple blocks may be hidden, be
|
29
|
+
# # sure to keep the "email-hidden-reply" <div>s around "email-quoted-reply"
|
30
|
+
# # <div> tags. Call this on each comment of a visible thread in the order
|
31
|
+
# # that they are displayed. Note: all comments are processed so we can
|
32
|
+
# # maintain a Set of SHAs of paragraphs. Only plaintext comments skip the
|
33
|
+
# # markdown step.
|
34
|
+
# #
|
35
|
+
# # Returns the email comment HTML as a String
|
36
|
+
# def call
|
37
|
+
# found_hidden = nil
|
38
|
+
# paragraphs = EmailReplyParser.read(text.dup).fragments.map do |fragment|
|
39
|
+
# pieces = [escape_html(fragment.to_s.strip).gsub(/^\s*(>|>)/, '')]
|
40
|
+
# if fragment.quoted?
|
41
|
+
# if context[:hide_quoted_email_addresses]
|
42
|
+
# pieces.map! do |piece|
|
43
|
+
# piece.gsub(EMAIL_REGEX, HIDDEN_EMAIL_PATTERN)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
# pieces.unshift EMAIL_QUOTED_HEADER
|
47
|
+
# pieces << EMAIL_HEADER_END
|
48
|
+
# elsif fragment.signature?
|
49
|
+
# pieces.unshift EMAIL_SIGNATURE_HEADER
|
50
|
+
# pieces << EMAIL_HEADER_END
|
51
|
+
# else
|
52
|
+
# pieces.unshift EMAIL_FRAGMENT_HEADER
|
53
|
+
# pieces << EMAIL_HEADER_END
|
54
|
+
# end
|
55
|
+
# if fragment.hidden? && !found_hidden
|
56
|
+
# found_hidden = true
|
57
|
+
# pieces.unshift EMAIL_HIDDEN_HEADER
|
58
|
+
# end
|
59
|
+
# pieces.join
|
60
|
+
# end
|
61
|
+
# paragraphs << EMAIL_HEADER_END if found_hidden
|
62
|
+
# paragraphs.join("\n")
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
# end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# require 'cgi'
|
2
|
+
# MotionHTMLPipeline::Pipeline.require_dependency('gemoji', 'EmojiFilter')
|
3
|
+
#
|
4
|
+
# module MotionHTMLPipeline
|
5
|
+
# class Pipeline
|
6
|
+
# # HTML filter that replaces :emoji: with images.
|
7
|
+
# #
|
8
|
+
# # Context:
|
9
|
+
# # :asset_root (required) - base url to link to emoji sprite
|
10
|
+
# # :asset_path (optional) - url path to link to emoji sprite. :file_name can be used as a placeholder for the sprite file name. If no asset_path is set "emoji/:file_name" is used.
|
11
|
+
# # :ignored_ancestor_tags (optional) - Tags to stop the emojification. Node has matched ancestor HTML tags will not be emojified. Default to pre, code, and tt tags. Extra tags please pass in the form of array, e.g., %w(blockquote summary).
|
12
|
+
# # :img_attrs (optional) - Attributes for generated img tag. E.g. Pass { "draggble" => true, "height" => nil } to set draggable attribute to "true" and clear height attribute of generated img tag.
|
13
|
+
# class EmojiFilter < Filter
|
14
|
+
# DEFAULT_IGNORED_ANCESTOR_TAGS = %w[pre code tt].freeze
|
15
|
+
#
|
16
|
+
# def call
|
17
|
+
# doc.search('.//text()').each do |node|
|
18
|
+
# content = node.text
|
19
|
+
# next unless content.include?(':')
|
20
|
+
# next if has_ancestor?(node, ignored_ancestor_tags)
|
21
|
+
# html = emoji_image_filter(content)
|
22
|
+
# next if html == content
|
23
|
+
# node.replace(html)
|
24
|
+
# end
|
25
|
+
# doc
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Implementation of validate hook.
|
29
|
+
# # Errors should raise exceptions or use an existing validator.
|
30
|
+
# def validate
|
31
|
+
# needs :asset_root
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Replace :emoji: with corresponding images.
|
35
|
+
# #
|
36
|
+
# # text - String text to replace :emoji: in.
|
37
|
+
# #
|
38
|
+
# # Returns a String with :emoji: replaced with images.
|
39
|
+
# def emoji_image_filter(text)
|
40
|
+
# text.gsub(emoji_pattern) do |_match|
|
41
|
+
# emoji_image_tag(Regexp.last_match(1))
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # The base url to link emoji sprites
|
46
|
+
# #
|
47
|
+
# # Raises ArgumentError if context option has not been provided.
|
48
|
+
# # Returns the context's asset_root.
|
49
|
+
# def asset_root
|
50
|
+
# context[:asset_root]
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # The url path to link emoji sprites
|
54
|
+
# #
|
55
|
+
# # :file_name can be used in the asset_path as a placeholder for the sprite file name. If no asset_path is set in the context "emoji/:file_name" is used.
|
56
|
+
# # Returns the context's asset_path or the default path if no context asset_path is given.
|
57
|
+
# def asset_path(name)
|
58
|
+
# if context[:asset_path]
|
59
|
+
# context[:asset_path].gsub(':file_name', emoji_filename(name))
|
60
|
+
# else
|
61
|
+
# File.join('emoji', emoji_filename(name))
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# private
|
66
|
+
#
|
67
|
+
# # Build an emoji image tag
|
68
|
+
# def emoji_image_tag(name)
|
69
|
+
# require 'active_support/core_ext/hash/indifferent_access'
|
70
|
+
# html_attrs =
|
71
|
+
# default_img_attrs(name)
|
72
|
+
# .merge!((context[:img_attrs] || {}).with_indifferent_access)
|
73
|
+
# .map { |attr, value| !value.nil? && %(#{attr}="#{value.respond_to?(:call) && value.call(name) || value}") }
|
74
|
+
# .reject(&:blank?).join(' '.freeze)
|
75
|
+
#
|
76
|
+
# "<img #{html_attrs}>"
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# # Default attributes for img tag
|
80
|
+
# def default_img_attrs(name)
|
81
|
+
# {
|
82
|
+
# 'class' => 'emoji'.freeze,
|
83
|
+
# 'title' => ":#{name}:",
|
84
|
+
# 'alt' => ":#{name}:",
|
85
|
+
# 'src' => emoji_url(name).to_s,
|
86
|
+
# 'height' => '20'.freeze,
|
87
|
+
# 'width' => '20'.freeze,
|
88
|
+
# 'align' => 'absmiddle'.freeze
|
89
|
+
# }
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# def emoji_url(name)
|
93
|
+
# File.join(asset_root, asset_path(name))
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# # Build a regexp that matches all valid :emoji: names.
|
97
|
+
# def self.emoji_pattern
|
98
|
+
# @emoji_pattern ||= /:(#{emoji_names.map { |name| Regexp.escape(name) }.join('|')}):/
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# def emoji_pattern
|
102
|
+
# self.class.emoji_pattern
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# def self.emoji_names
|
106
|
+
# Emoji.all.map(&:aliases).flatten.sort
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# def emoji_filename(name)
|
110
|
+
# Emoji.find_by_alias(name).image_filename
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# # Return ancestor tags to stop the emojification.
|
114
|
+
# #
|
115
|
+
# # @return [Array<String>] Ancestor tags.
|
116
|
+
# def ignored_ancestor_tags
|
117
|
+
# if context[:ignored_ancestor_tags]
|
118
|
+
# DEFAULT_IGNORED_ANCESTOR_TAGS | context[:ignored_ancestor_tags]
|
119
|
+
# else
|
120
|
+
# DEFAULT_IGNORED_ANCESTOR_TAGS
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
# end
|