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 return '' if raw_content
.blank
?
25 html
= reformat(raw_content
)
26 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
27 return html
.html_safe
# rubocop:disable Rails/OutputSafety
30 linkable_accounts
= status
.active_mentions
.map(&:account)
31 linkable_accounts
<< status
.account
34 html
= "RT @#{prepend_reblog} #{html}" if prepend_reblog
35 html
= encode_and_link_urls(html
, linkable_accounts
)
36 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
37 html
= simple_format(html
, {}, sanitize
: false)
38 html
= html
.delete("\n")
40 html
.html_safe
# rubocop:disable Rails/OutputSafety
44 sanitize(html
, Sanitize
::Config::MASTODON_STRICT)
48 return status
.text
if status
.local
?
50 text
= status
.text
.gsub(/(<br \/>|<br
>|<\
/p>)+/) { |match
| "#{match}\n" }
54 def simplified_format(account
, **options
)
55 html
= account
.local
? ? linkify(account
.note
) : reformat(account
.note
)
56 html
= encode_custom_emojis(html
, account
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
57 html
.html_safe
# rubocop:disable Rails/OutputSafety
60 def sanitize(html
, config
)
61 Sanitize
.fragment(html
, config
)
64 def format_spoiler(status
, **options
)
65 html
= encode(status
.spoiler_text
)
66 html
= encode_custom_emojis(html
, status
.emojis
, options
[:autoplay])
67 html
.html_safe
# rubocop:disable Rails/OutputSafety
70 def format_display_name(account
, **options
)
71 html
= encode(account
.display_name
.presence
|| account
.username
)
72 html
= encode_custom_emojis(html
, account
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
73 html
.html_safe
# rubocop:disable Rails/OutputSafety
76 def format_field(account
, str
, **options
)
77 return reformat(str
).html_safe
unless account
.local
? # rubocop:disable Rails/OutputSafety
78 html
= encode_and_link_urls(str
, me
: true)
79 html
= encode_custom_emojis(html
, account
.emojis
, options
[:autoplay]) if options
[:custom_emojify]
80 html
.html_safe
# rubocop:disable Rails/OutputSafety
84 html
= encode_and_link_urls(text
)
85 html
= simple_format(html
, {}, sanitize
: false)
86 html
= html
.delete("\n")
88 html
.html_safe
# rubocop:disable Rails/OutputSafety
94 @html_entities ||= HTMLEntities
.new
98 html_entities
.encode(html
)
101 def encode_and_link_urls(html
, accounts
= nil, options
= {})
102 entities
= utf8_friendly_extractor(html
, extract_url_without_protocol
: false)
104 if accounts
.is_a
?(Hash
)
109 rewrite(html
.dup
, entities
) do |entity
|
111 link_to_url(entity
, options
)
112 elsif entity
[:hashtag]
113 link_to_hashtag(entity
)
114 elsif entity
[:screen_name]
115 link_to_mention(entity
, accounts
)
120 def count_tag_nesting(tag
)
121 if tag
[1] == '/' then -1
122 elsif tag
[-2] == '/' then 0
127 def encode_custom_emojis(html
, emojis
, animate
= false)
128 return html
if emojis
.empty
?
130 emoji_map
= if animate
131 emojis
.each_with_object({}) { |e
, h
| h
[e
.shortcode
] = full_asset_url(e
.image
.url
) }
133 emojis
.each_with_object({}) { |e
, h
| h
[e
.shortcode
] = full_asset_url(e
.image
.url(:static)) }
138 inside_shortname
= false
139 shortname_start_index
= -1
142 while i +
1 < html
.size
145 if invisible_depth
.zero
? && inside_shortname
&& html
[i
] == ':'
146 shortcode
= html
[shortname_start_index +
1..i
- 1]
147 emoji
= emoji_map
[shortcode
]
150 replacement
= "<img draggable=\"false\" class=\"emojione\" alt=\":#{encode(shortcode)}:\" title=\":#{encode(shortcode)}:\" src=\"#{encode(emoji)}\" />"
151 before_html
= shortname_start_index
.positive
? ? html
[0..shortname_start_index
- 1] : ''
152 html
= before_html + replacement + html
[i +
1..-1]
153 i +
= replacement
.size
- (shortcode
.size +
2) - 1
158 inside_shortname
= false
159 elsif tag_open_index
&& html
[i
] == '>'
160 tag
= html
[tag_open_index
..i
]
162 if invisible_depth
.positive
?
163 invisible_depth +
= count_tag_nesting(tag
)
164 elsif tag
== '<span class="invisible">'
169 inside_shortname
= false
170 elsif !tag_open_index
&& html
[i
] == ':'
171 inside_shortname
= true
172 shortname_start_index
= i
179 def rewrite(text
, entities
)
180 chars
= text
.to_s
.to_char_a
182 # Sort by start index
183 entities
= entities
.sort_by
do |entity
|
184 indices
= entity
.respond_to
?(:indices) ? entity
.indices
: entity
[:indices]
190 last_index
= entities
.reduce(0) do |index
, entity
|
191 indices
= entity
.respond_to
?(:indices) ? entity
.indices
: entity
[:indices]
192 result
<< encode(chars
[index
...indices
.first
].join
)
193 result
<< yield(entity
)
197 result
<< encode(chars
[last_index
..-1].join
)
202 UNICODE_ESCAPE_BLACKLIST_RE
= /\p{Z}|\p{P}/
204 def utf8_friendly_extractor(text
, options
= {})
205 old_to_new_index
= [0]
207 escaped
= text
.chars
.map
do |c
|
209 if c
.ord
.to_s(16).length
> 2 && UNICODE_ESCAPE_BLACKLIST_RE
.match(c
).nil?
216 old_to_new_index
<< old_to_new_index
.last + output
.length
221 # Note: I couldn't obtain list_slug with @user/list-name format
222 # for mention so this requires additional check
223 special
= Extractor
.extract_urls_with_indices(escaped
, options
).map
do |extract
|
224 # exactly one of :url, :hashtag, :screen_name, :cashtag keys is present
225 key
= (extract
.keys
& [:url, :hashtag, :screen_name, :cashtag]).first
228 old_to_new_index
.find_index(extract
[:indices].first
),
229 old_to_new_index
.find_index(extract
[:indices].last
),
232 has_prefix_char
= [:hashtag, :screen_name, :cashtag].include?(key
)
234 new_indices
.first +
(has_prefix_char
? 1 : 0), # account for #, @ or $
235 new_indices
.last
- 1,
239 :indices => new_indices
,
240 key
=> text
[value_indices
.first
..value_indices
.last
]
244 standard
= Extractor
.extract_entities_with_indices(text
, options
)
246 Extractor
.remove_overlapping_entities(special + standard
)
249 def link_to_url(entity
, options
= {})
250 url
= Addressable
::URI.parse(entity
[:url])
251 html_attrs
= { target
: '_blank', rel
: 'nofollow noopener' }
253 html_attrs
[:rel] = "me #{html_attrs[:rel]}" if options
[:me]
255 Twitter
::Autolink.send(:link_to_text, entity
, link_html(entity
[:url]), url
, html_attrs
)
256 rescue Addressable
::URI::InvalidURIError, IDN
::Idna::IdnaError
260 def link_to_mention(entity
, linkable_accounts
)
261 acct
= entity
[:screen_name]
263 return link_to_account(acct
) unless linkable_accounts
265 account
= linkable_accounts
.find
{ |item
| TagManager
.instance
.same_acct
?(item
.acct
, acct
) }
266 account
? mention_html(account
) : "@#{encode(acct)}"
269 def link_to_account(acct
)
270 username
, domain
= acct
.split('@')
272 domain
= nil if TagManager
.instance
.local_domain
?(domain
)
273 account
= EntityCache
.instance
.mention(username
, domain
)
275 account
? mention_html(account
) : "@#{encode(acct)}"
278 def link_to_hashtag(entity
)
279 hashtag_html(entity
[:hashtag])
283 url
= Addressable
::URI.parse(url
).to_s
284 prefix
= url
.match(/\Ahttps?:\/\
/(www\.)?/).to_s
285 text
= url
[prefix
.length
, 30]
286 suffix
= url
[prefix
.length +
30..-1]
287 cutoff
= url
[prefix
.length
..-1].length
> 30
289 "<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>"
292 def hashtag_html(tag
)
293 "<a href=\"#{encode(tag_url(tag.downcase))}\" class=\"mention hashtag\" rel=\"tag\">#<span>#{encode(tag)}</span></a>"
296 def mention_html(account
)
297 "<span class=\"h-card\"><a href=\"#{encode(TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"