1 # frozen_string_literal: true
4 require_relative
'./sanitize_config'
10 include ActionView
::Helpers::TextHelper
12 def format(status
, **options
)
14 prepend_reblog
= status
.reblog
.account
.acct
15 status
= status
.proper
17 prepend_reblog
= false
20 raw_content
= status
.text
22 if options
[:inline_poll_options] && status
.preloadable_poll
23 raw_content
= raw_content +
"\n\n" + status
.preloadable_poll
.options
.map
{ |title
| "[ ] #{title}" }.join("\n")
26 return '' if raw_content
.blank
?
29 html
= reformat(raw_content
)
30 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
31 return html
.html_safe
# rubocop:disable Rails/OutputSafety
34 linkable_accounts
= status
.active_mentions
.map(&:account)
35 linkable_accounts
<< status
.account
38 html
= "RT @#{prepend_reblog} #{html}" if prepend_reblog
39 html
= encode_and_link_urls(html
, linkable_accounts
)
40 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
41 html
= simple_format(html
, {}, sanitize
: false)
42 html
= html
.delete("\n")
44 html
.html_safe
# rubocop:disable Rails/OutputSafety
48 sanitize(html
, Sanitize
::Config::MASTODON_STRICT)
54 return status
.text
if status
.local
?
56 text
= status
.text
.gsub(/(<br \/>|<br
>|<\
/p>)+/) { |match
| "#{match}\n" }
60 def simplified_format(account
, **options
)
61 html
= account
.local
? ? linkify(account
.note
) : reformat(account
.note
)
62 html
= encode_custom_emojis(html
, account
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
63 html
.html_safe
# rubocop:disable Rails/OutputSafety
66 def sanitize(html
, config
)
67 Sanitize
.fragment(html
, config
)
70 def format_spoiler(status
, **options
)
71 html
= encode(status
.spoiler_text
)
72 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay])
73 html
.html_safe
# rubocop:disable Rails/OutputSafety
76 def format_poll_option(status
, option
, **options
)
77 html
= encode(option
.title
)
78 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay])
79 html
.html_safe
# rubocop:disable Rails/OutputSafety
82 def format_display_name(account
, **options
)
83 html
= encode(account
.display_name
.presence
|| account
.username
)
84 html
= encode_custom_emojis(html
, account
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
85 html
.html_safe
# rubocop:disable Rails/OutputSafety
88 def format_field(account
, str
, **options
)
89 html
= account
.local
? ? encode_and_link_urls(str
, me
: true) : reformat(str
)
90 html
= encode_custom_emojis(html
, account
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
91 html
.html_safe
# rubocop:disable Rails/OutputSafety
95 html
= encode_and_link_urls(text
)
96 html
= simple_format(html
, {}, sanitize
: false)
97 html
= html
.delete("\n")
99 html
.html_safe
# rubocop:disable Rails/OutputSafety
105 @html_entities ||= HTMLEntities
.new
109 html_entities
.encode(html
)
112 def encode_and_link_urls(html
, accounts
= nil, options
= {})
113 entities
= utf8_friendly_extractor(html
, extract_url_without_protocol
: false)
115 if accounts
.is_a
?(Hash
)
120 rewrite(html
.dup
, entities
) do |entity
|
122 link_to_url(entity
, options
)
123 elsif entity
[:hashtag]
124 link_to_hashtag(entity
)
125 elsif entity
[:screen_name]
126 link_to_mention(entity
, accounts
)
131 def count_tag_nesting(tag
)
132 if tag
[1] == '/' then -1
133 elsif tag
[-2] == '/' then 0
138 # rubocop:disable Metrics/BlockNesting
139 def encode_custom_emojis(html
, emojis
, animate
= false)
140 return html
if emojis
.empty
?
142 emoji_map
= emojis
.each_with_object({}) { |e
, h
| h
[e
.shortcode
] = [full_asset_url(e
.image
.url
), full_asset_url(e
.image
.url(:static))] }
146 inside_shortname
= false
147 shortname_start_index
= -1
150 while i +
1 < html
.size
153 if invisible_depth
.zero
? && inside_shortname
&& html
[i
] == ':'
154 shortcode
= html
[shortname_start_index +
1..i
- 1]
155 emoji
= emoji_map
[shortcode
]
158 original_url
, static_url
= emoji
161 "<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(original_url)}\" />"
163 "<img draggable=\"false\" class=\"emojione custom-emoji\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(static_url)}\" data-original=\"#{original_url}\" data-static=\"#{static_url}\" />"
166 before_html
= shortname_start_index
.positive
? ? html
[0..shortname_start_index
- 1] : ''
167 html
= before_html + replacement + html
[i +
1..-1]
168 i +
= replacement
.size
- (shortcode
.size +
2) - 1
173 inside_shortname
= false
174 elsif tag_open_index
&& html
[i
] == '>'
175 tag
= html
[tag_open_index
..i
]
177 if invisible_depth
.positive
?
178 invisible_depth +
= count_tag_nesting(tag
)
179 elsif tag
== '<span class="invisible">'
184 inside_shortname
= false
185 elsif !tag_open_index
&& html
[i
] == ':'
186 inside_shortname
= true
187 shortname_start_index
= i
193 # rubocop:enable Metrics/BlockNesting
195 def rewrite(text
, entities
)
198 # Sort by start index
199 entities
= entities
.sort_by
do |entity
|
200 indices
= entity
.respond_to
?(:indices) ? entity
.indices
: entity
[:indices]
206 last_index
= entities
.reduce(0) do |index
, entity
|
207 indices
= entity
.respond_to
?(:indices) ? entity
.indices
: entity
[:indices]
208 result
<< encode(text
[index
...indices
.first
])
209 result
<< yield(entity
)
213 result
<< encode(text
[last_index
..-1])
218 UNICODE_ESCAPE_BLACKLIST_RE
= /\p{Z}|\p{P}/
220 def utf8_friendly_extractor(text
, options
= {})
221 old_to_new_index
= [0]
223 escaped
= text
.chars
.map
do |c
|
225 if c
.ord
.to_s(16).length
> 2 && !UNICODE_ESCAPE_BLACKLIST_RE
.match
?(c
)
232 old_to_new_index
<< old_to_new_index
.last + output
.length
237 # Note: I couldn't obtain list_slug with @user/list-name format
238 # for mention so this requires additional check
239 special
= Extractor
.extract_urls_with_indices(escaped
, options
).map
do |extract
|
241 old_to_new_index
.find_index(extract
[:indices].first
),
242 old_to_new_index
.find_index(extract
[:indices].last
),
246 indices
: new_indices
,
247 url
: text
[new_indices
.first
..new_indices
.last
- 1]
251 standard
= Extractor
.extract_entities_with_indices(text
, options
)
252 extra
= Extractor
.extract_extra_uris_with_indices(text
, options
)
254 Extractor
.remove_overlapping_entities(special + standard + extra
)
257 def link_to_url(entity
, options
= {})
258 url
= Addressable
::URI.parse(entity
[:url])
259 html_attrs
= { target
: '_blank', rel
: 'nofollow noopener noreferrer' }
261 html_attrs
[:rel] = "me #{html_attrs[:rel]}" if options
[:me]
263 Twitter
::Autolink.send(:link_to_text, entity
, link_html(entity
[:url]), url
, html_attrs
)
264 rescue Addressable
::URI::InvalidURIError, IDN
::Idna::IdnaError
268 def link_to_mention(entity
, linkable_accounts
)
269 acct
= entity
[:screen_name]
271 return link_to_account(acct
) unless linkable_accounts
273 account
= linkable_accounts
.find
{ |item
| TagManager
.instance
.same_acct
?(item
.acct
, acct
) }
274 account
? mention_html(account
) : "@#{encode(acct)}"
277 def link_to_account(acct
)
278 username
, domain
= acct
.split('@')
280 domain
= nil if TagManager
.instance
.local_domain
?(domain
)
281 account
= EntityCache
.instance
.mention(username
, domain
)
283 account
? mention_html(account
) : "@#{encode(acct)}"
286 def link_to_hashtag(entity
)
287 hashtag_html(entity
[:hashtag])
291 url
= Addressable
::URI.parse(url
).to_s
292 prefix
= url
.match(/\A(https?:\/\
/(www\.)?|xmpp:)/).to_s
293 text
= url
[prefix
.length
, 30]
294 suffix
= url
[prefix
.length +
30..-1]
295 cutoff
= url
[prefix
.length
..-1].length
> 30
297 "<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>"
300 def hashtag_html(tag
)
301 "<a href=\"#{encode(tag_url(tag))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
304 def mention_html(account
)
305 "<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"