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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
|
# -*- coding: utf-8 -*-
miquire :core, 'message'
miquire :core, 'userconfig'
miquire :lib, 'addressable/uri'
class Message::Entity
ESCAPE_RULE = {'&' => '&'.freeze ,'>' => '>'.freeze, '<' => '<'.freeze}.freeze
UNESCAPE_RULE = ESCAPE_RULE.invert.freeze
REGEXP_EACH_CHARACTER = //u.freeze
REGEXP_ENTITY_ENCODE_TARGET = Regexp.union(*ESCAPE_RULE.keys.map(&Regexp.method(:escape))).freeze
REGEXP_ENTITY_DECODE_TARGET = Regexp.union(*ESCAPE_RULE.values.map(&Regexp.method(:escape))).freeze
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
# "look http://example.com/" のようなツイートに対して、
# ["l", "o", "o", "k", " ", {エンティティのURLの値}]
# のように、エンティティの情報を間に入れた配列にして返す。
def segment_splitted
result = message.to_show.split(REGEXP_EACH_CHARACTER)
reverse_each{ |segment|
result[segment[:range]] = segment }
result.freeze end
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
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 = Regexp.last_match
pos = match.begin(0)
body = match.to_s.freeze
if not result.any?{ |this| this[:range].include?(pos) }
result << @@filter[rule[:slug]].call(rule.merge({ :message => message,
:range => Range.new(pos, pos + body.size, true),
:face => body,
:from => :_generate_value,
:url => body})).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(self.class.index_to_escaped_index(message.to_show, indices[0]),
self.class.index_to_escaped_index(message.to_show, indices[1]), true) end
# to_showで得たエンティティデコードされた文字列の index が、
# エンティティエンコードされた文字列ではどうなるかを返す。
# ==== Args
# [decoded_string] String デコードされた文字列
# [encoded_index] Fixnum エンコード済み文字列でのインデックス
# ==== Return
# Fixnum デコード済み文字列でのインデックス
def self.index_to_escaped_index(decoded_string, encoded_index)
decoded_string
.gsub(REGEXP_ENTITY_ENCODE_TARGET, &ESCAPE_RULE.method(:[]))
.slice(0, encoded_index)
.gsub(REGEXP_ENTITY_DECODE_TARGET, &UNESCAPE_RULE.method(:[]))
.size end
# decoded_string
# .split(REGEXP_EACH_CHARACTER)
# .map{ |s| ESCAPE_RULE[s] || s }
# .join
# .split(REGEXP_EACH_CHARACTER)[0, encoded_index]
# .join
# .gsub(/&.+?;/, '.'.freeze)
# .size end
class InvalidEntityError < Message::MessageError
end
refresh
end
|