1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
|
# -*- coding: utf-8 -*-
miquire :core, 'message'
miquire :core, 'userconfig'
miquire :lib, 'addressable/uri'
class Message::Entity
include Enumerable
attr_reader :message
def self.addlinkrule(slug, regexp=nil, filter_id=nil, &callback)
slug = slug.to_sym
Plugin.call(:entity_linkrule_added, { slug: slug, filter_id: filter_id, regexp: regexp, callback: callback }.freeze)
# Gtk::IntelligentTextview.addlinkrule(regexp, lambda{ |seg, tv| callback.call(face: seg, url: seg, textview: tv) }) if regexp
self end
def self.on_entity_linkrule_added(linkrule)
@@linkrule[linkrule[:slug]] = linkrule
self end
def self.filter(slug, &filter)
if @@filter.has_key?(slug)
parent = @@filter[slug]
@@filter[slug] = filter_wrap{ |s| filter.call(parent.call(s)) }
else
@@filter[slug] = filter_wrap &filter end
self end
def self.filter_wrap(&filter)
->(s) {
result = filter.call(s)
[:url, :face].each{ |key|
if defined? result[:message]
raise InvalidEntityError.new("entity key :#{key} required. but not exist", result[:message]) unless result[key]
else
raise RuntimeError, "entity key :#{key} required. but not exist" end }
result } end
def self.refresh
@@linkrule = {}
@@filter = Hash.new(filter_wrap(&ret_nth))
filter(:urls){ |segment|
segment[:face] ||= segment[:url]
if UserConfig[:shrinkurl_expand]
url = segment[:expanded_url] || segment[:url]
if MessageConverters.shrinked_url? url
segment[:face] = MessageConverters.expand_url([url])[url]
elsif segment[:expanded_url]
begin
normalized = Addressable::URI.parse('//'+segment[:display_url]).display_uri.to_s
segment[:face] = normalized[2, normalized.size]
rescue => e
error e
segment[:face] = segment[:display_url] end end end
segment }
filter(:media){ |segment|
segment[:face] = segment[:display_url]
segment[:url] = segment[:media_url]
segment }
filter(:hashtags){ |segment|
segment[:face] ||= "#"+segment[:text]
segment[:url] ||= "#"+segment[:text]
segment }
filter(:user_mentions){ |segment|
segment[:face] ||= "@"+segment[:screen_name]
segment[:url] ||= "@"+segment[:screen_name]
segment }
end
def initialize(message)
type_strict message => Message
@message = message
@generate_thread = Thread.new {
begin
@generate_value = _generate_value || []
rescue TimeoutError => e
error "entity parse timeout. ##{message[:id]}(@#{message.user[:idname]}: #{message.to_show})"
raise RuntimeError, "entity parse timeout. ##{message[:id]}(@#{message.user[:idname]}: #{message.to_show})"
rescue Exception => e
error e end
@generate_thread = nil } end
def each
to_a.each(&Proc.new)
end
def reverse_each
to_a.reverse.each(&Proc.new)
end
# [{range: リンクを貼る場所のRange, face: 表示文字列, url:リンク先}, ...] の配列を返す
# face: TLに印字される文字列。
# url: 実際のリンク先。本当にURLになるかはリンクの種類に依存する。
# 例えばハッシュタグ "#mikutter" の場合はこの内容は "mikutter" になる。
def to_a
generate_value end
# entityフィルタを適用した状態のMessageの本文を返す
def to_s
segment_splitted.map{ |s|
if s.is_a? Hash
s[:face]
else
s end }.join end
# _index_ 文字目のエンティティの要素を返す。エンティティでなければnilを返す
def segment_by_index(index)
segment_text.each{ |segment|
if segment.is_a? Integer
index -= segment
elsif segment.is_a? Hash
index -= segment[:face].size
end
if index < 0
if segment.is_a? Hash
return segment
else
return nil end end }
nil end
private
# "look http://example.com/" のようなツイートに対して、
# ["l", "o", "o", "k", " ", {エンティティのURLの値}]
# のように、エンティティの情報を間に入れた配列にして返す。
def segment_splitted
result = message.to_show.split(//u)
reverse_each{ |segment|
result[segment[:range]] = segment }
result.freeze end
memoize :segment_splitted
def segment_text
result = []
segment_splitted.each{ |segment|
if segment.is_a? String
if result.last.is_a? Integer
result[-1] += 1
else
result << 1 end
elsif segment.is_a? Hash
result << segment end }
result.freeze end
memoize :segment_text
def generate_value
@generate_thread.join if @generate_thread
@generate_value end
def _generate_value
result = Set.new(message_entities)
@@linkrule.values.each{ |rule|
if rule[:regexp]
message.to_show.scan(rule[:regexp]){ |match|
pos = Regexp.last_match.begin(0)
if not result.any?{ |this| this[:range].include?(pos) }
result << @@filter[rule[:slug]].call(rule.merge({ :message => message,
:range => Range.new(pos, pos + match.to_s.size, true),
:face => match.to_s,
:from => :_generate_value,
:url => match.to_s})).freeze end } end }
result.sort_by{ |r| r[:range].first }.freeze end
# Messageオブジェクトに含まれるentity情報を、 Message::Entity#to_a と同じ形式で返す。
def message_entities
result = Set.new
if message[:entities]
message[:entities].each{ |slug, children|
children.each{ |link|
begin
rule = @@linkrule[slug] || {}
entity = @@filter[slug].call(rule.merge({ :message => message,
:from => :message_entities}.merge(link)))
entity[:range] = get_range_by_face(entity) #indices_to_range(link[:indices])
result << entity.freeze
rescue InvalidEntityError, RuntimeError => exception
error exception end } } end
result.sort_by{ |r| r[:range].first }.freeze end
def get_range_by_face(link)
right = message.to_show.index(link[:url], link[:indices][0])
left = message.to_show.rindex(link[:url], link[:indices][1])
if right and left
start = [right - link[:indices][0], left - link[:indices][0]].map(&:abs).min + link[:indices][0]
start...(start + link[:url].size)
elsif right or left
start = right || left
start...(start + link[:url].size)
else
indices_to_range(link[:indices]) end end
def indices_to_range(indices)
Range.new(index_to_escaped_index(indices[0]), index_to_escaped_index(indices[1]), true) end
def index_to_escaped_index(index)
escape_rule = {'>' => '>', '<' => '<'}
message.to_show.split(//u).map{ |s|
escape_rule[s] || s }.join.split(//u)[0, index].join.gsub(/&.+?;/, '.').size
end
class InvalidEntityError < Message::MessageError
end
refresh
end
|